Refactor group store

This commit is contained in:
AsamK 2021-05-02 16:02:54 +02:00
parent a1014ba39c
commit 5b8c0c4e2d
14 changed files with 644 additions and 473 deletions

View file

@ -265,6 +265,10 @@ public class Manager implements Closeable {
return account.getSelfAddress(); return account.getSelfAddress();
} }
public RecipientId getSelfRecipientId() {
return account.getSelfRecipientId();
}
private IdentityKeyPair getIdentityKeyPair() { private IdentityKeyPair getIdentityKeyPair() {
return account.getIdentityKeyPair(); return account.getIdentityKeyPair();
} }
@ -673,7 +677,7 @@ public class Manager implements Closeable {
if (g == null) { if (g == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
} }
if (!g.isMember(account.getSelfAddress())) { if (!g.isMember(account.getSelfRecipientId())) {
throw new NotAGroupMemberException(groupId, g.getTitle()); throw new NotAGroupMemberException(groupId, g.getTitle());
} }
return g; return g;
@ -684,7 +688,7 @@ public class Manager implements Closeable {
if (g == null) { if (g == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
} }
if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) { if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
throw new NotAGroupMemberException(groupId, g.getTitle()); throw new NotAGroupMemberException(groupId, g.getTitle());
} }
return g; return g;
@ -725,7 +729,7 @@ public class Manager implements Closeable {
GroupUtils.setGroupContext(messageBuilder, g); GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime()); messageBuilder.withExpiration(g.getMessageExpirationTime());
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
} }
public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
@ -736,17 +740,17 @@ public class Manager implements Closeable {
var groupInfoV1 = (GroupInfoV1) g; var groupInfoV1 = (GroupInfoV1) g;
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build(); var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build();
messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
groupInfoV1.removeMember(account.getSelfAddress()); groupInfoV1.removeMember(account.getSelfRecipientId());
account.getGroupStore().updateGroup(groupInfoV1); account.getGroupStore().updateGroup(groupInfoV1);
} else { } else {
final var groupInfoV2 = (GroupInfoV2) g; final var groupInfoV2 = (GroupInfoV2) g;
final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2); final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
groupInfoV2.setGroup(groupGroupChangePair.first()); groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
account.getGroupStore().updateGroup(groupInfoV2); account.getGroupStore().updateGroup(groupInfoV2);
} }
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
} }
public Pair<GroupId, List<SendMessageResult>> updateGroup( public Pair<GroupId, List<SendMessageResult>> updateGroup(
@ -754,11 +758,7 @@ public class Manager implements Closeable {
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId, return sendUpdateGroupMessage(groupId,
name, name,
members == null members == null ? null : getSignalServiceAddresses(members),
? null
: getSignalServiceAddresses(members).stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()),
avatarFile); avatarFile);
} }
@ -769,16 +769,20 @@ public class Manager implements Closeable {
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) { if (groupId == null) {
// Create new group // Create new group
var gv2 = groupHelper.createGroupV2(name == null ? "" : name, var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
members == null ? Set.of() : members, members == null ? Set.of() : members,
avatarFile); avatarFile);
if (gv2 == null) { if (gv2Pair == null) {
var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
gv1.addMembers(List.of(account.getSelfAddress())); gv1.addMembers(List.of(account.getSelfRecipientId()));
updateGroupV1(gv1, name, members, avatarFile); updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1); messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1; g = gv1;
} else { } else {
final var gv2 = gv2Pair.first();
final var decryptedGroup = gv2Pair.second();
gv2.setGroup(decryptedGroup, this::resolveRecipient);
if (avatarFile != null) { if (avatarFile != null) {
avatarStore.storeGroupAvatar(gv2.getGroupId(), avatarStore.storeGroupAvatar(gv2.getGroupId(),
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
@ -792,7 +796,7 @@ public class Manager implements Closeable {
final var groupInfoV2 = (GroupInfoV2) group; final var groupInfoV2 = (GroupInfoV2) group;
Pair<Long, List<SendMessageResult>> result = null; Pair<Long, List<SendMessageResult>> result = null;
if (groupInfoV2.isPendingMember(getSelfAddress())) { if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
result = sendUpdateGroupMessage(groupInfoV2, result = sendUpdateGroupMessage(groupInfoV2,
groupGroupChangePair.first(), groupGroupChangePair.first(),
@ -801,10 +805,7 @@ public class Manager implements Closeable {
if (members != null) { if (members != null) {
final var newMembers = new HashSet<>(members); final var newMembers = new HashSet<>(members);
newMembers.removeAll(group.getMembers() newMembers.removeAll(group.getMembers());
.stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()));
if (newMembers.size() > 0) { if (newMembers.size() > 0) {
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
result = sendUpdateGroupMessage(groupInfoV2, result = sendUpdateGroupMessage(groupInfoV2,
@ -834,7 +835,8 @@ public class Manager implements Closeable {
account.getGroupStore().updateGroup(g); account.getGroupStore().updateGroup(g);
final var result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress())); final var result = sendMessage(messageBuilder,
g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
return new Pair<>(g.getGroupId(), result.second()); return new Pair<>(g.getGroupId(), result.second());
} }
@ -846,12 +848,13 @@ public class Manager implements Closeable {
} }
if (members != null) { if (members != null) {
final var memberAddresses = members.stream() final var newMemberAddresses = members.stream()
.filter(member -> !g.isMember(member))
.map(this::resolveSignalServiceAddress) .map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()); .collect(Collectors.toList());
final var newE164Members = new HashSet<String>(); final var newE164Members = new HashSet<String>();
for (var member : memberAddresses) { for (var member : newMemberAddresses) {
if (g.isMember(member) || !member.getNumber().isPresent()) { if (!member.getNumber().isPresent()) {
continue; continue;
} }
newE164Members.add(member.getNumber().get()); newE164Members.add(member.getNumber().get());
@ -866,7 +869,7 @@ public class Manager implements Closeable {
+ " to group: Not registered on Signal"); + " to group: Not registered on Signal");
} }
g.addMembers(memberAddresses); g.addMembers(members);
} }
if (avatarFile != null) { if (avatarFile != null) {
@ -928,10 +931,10 @@ public class Manager implements Closeable {
private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage( private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
) throws IOException { ) throws IOException {
group.setGroup(newDecryptedGroup); group.setGroup(newDecryptedGroup, this::resolveRecipient);
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
account.getGroupStore().updateGroup(group); account.getGroupStore().updateGroup(group);
return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress())); return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
} }
Pair<Long, List<SendMessageResult>> sendGroupInfoMessage( Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
@ -944,21 +947,24 @@ public class Manager implements Closeable {
} }
g = (GroupInfoV1) group; g = (GroupInfoV1) group;
if (!g.isMember(recipient)) { if (!g.isMember(resolveRecipient(recipient))) {
throw new NotAGroupMemberException(groupId, g.name); throw new NotAGroupMemberException(groupId, g.name);
} }
var messageBuilder = getGroupUpdateMessageBuilder(g); var messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it // Send group message only to the recipient who requested it
return sendMessage(messageBuilder, List.of(recipient)); return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
} }
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.getGroupId().serialize()) .withId(g.getGroupId().serialize())
.withName(g.name) .withName(g.name)
.withMembers(new ArrayList<>(g.getMembers())); .withMembers(g.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()));
try { try {
final var attachment = createGroupAvatarAttachment(g.getGroupId()); final var attachment = createGroupAvatarAttachment(g.getGroupId());
@ -991,7 +997,7 @@ public class Manager implements Closeable {
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
// Send group info request message to the recipient who sent us a message with this groupId // Send group info request message to the recipient who sent us a message with this groupId
return sendMessage(messageBuilder, List.of(recipient)); return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
} }
void sendReceipt( void sendReceipt(
@ -1126,9 +1132,9 @@ public class Manager implements Closeable {
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
} }
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException {
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
sendMessage(messageBuilder, List.of(address)); sendMessage(messageBuilder, Set.of(recipientId));
} }
/** /**
@ -1139,7 +1145,7 @@ public class Manager implements Closeable {
) throws IOException, InvalidNumberException { ) throws IOException, InvalidNumberException {
var recipientId = canonicalizeAndResolveRecipient(number); var recipientId = canonicalizeAndResolveRecipient(number);
setExpirationTimer(recipientId, messageExpirationTimer); setExpirationTimer(recipientId, messageExpirationTimer);
sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId)); sendExpirationTimerUpdate(recipientId);
account.save(); account.save();
} }
@ -1266,7 +1272,7 @@ public class Manager implements Closeable {
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync()); messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
} }
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException { private Set<RecipientId> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size()); final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size());
final var addressesMissingUuid = new HashSet<SignalServiceAddress>(); final var addressesMissingUuid = new HashSet<SignalServiceAddress>();
@ -1302,7 +1308,7 @@ public class Manager implements Closeable {
} }
} }
return signalServiceAddresses; return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
} }
private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException { private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
@ -1316,10 +1322,8 @@ public class Manager implements Closeable {
} }
private Pair<Long, List<SendMessageResult>> sendMessage( private Pair<Long, List<SendMessageResult>> sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
) throws IOException { ) throws IOException {
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet());
final var timestamp = System.currentTimeMillis(); final var timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe(); getOrCreateMessagePipe();
@ -1331,8 +1335,12 @@ public class Manager implements Closeable {
try { try {
var messageSender = createMessageSender(); var messageSender = createMessageSender();
final var isRecipientUpdate = false; final var isRecipientUpdate = false;
var result = messageSender.sendMessage(new ArrayList<>(recipients), final var recipientIdList = new ArrayList<>(recipientIds);
unidentifiedAccessHelper.getAccessFor(recipientIds), final var addresses = recipientIdList.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList());
var result = messageSender.sendMessage(addresses,
unidentifiedAccessHelper.getAccessFor(recipientIdList),
isRecipientUpdate, isRecipientUpdate,
message); message);
@ -1352,19 +1360,19 @@ public class Manager implements Closeable {
} else { } else {
// Send to all individually, so sync messages are sent correctly // Send to all individually, so sync messages are sent correctly
messageBuilder.withProfileKey(account.getProfileKey().serialize()); messageBuilder.withProfileKey(account.getProfileKey().serialize());
var results = new ArrayList<SendMessageResult>(recipients.size()); var results = new ArrayList<SendMessageResult>(recipientIds.size());
for (var address : recipients) { for (var recipientId : recipientIds) {
final var contact = account.getContactStore().getContact(resolveRecipient(address)); final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpiration(expirationTime);
message = messageBuilder.build(); message = messageBuilder.build();
results.add(sendMessage(address, message)); results.add(sendMessage(resolveSignalServiceAddress(recipientId), message));
} }
return new Pair<>(timestamp, results); return new Pair<>(timestamp, results);
} }
} finally { } finally {
if (message != null && message.isEndSession()) { if (message != null && message.isEndSession()) {
for (var recipient : recipients) { for (var recipient : recipientIds) {
handleEndSession(recipient); handleEndSession(recipient);
} }
} }
@ -1448,8 +1456,8 @@ public class Manager implements Closeable {
} }
} }
private void handleEndSession(SignalServiceAddress source) { private void handleEndSession(RecipientId recipientId) {
account.getSessionStore().deleteAllSessions(source.getIdentifier()); account.getSessionStore().deleteAllSessions(recipientId);
} }
private List<HandleAction> handleSignalServiceDataMessage( private List<HandleAction> handleSignalServiceDataMessage(
@ -1486,7 +1494,7 @@ public class Manager implements Closeable {
groupV1.addMembers(groupInfo.getMembers() groupV1.addMembers(groupInfo.getMembers()
.get() .get()
.stream() .stream()
.map(this::resolveSignalServiceAddress) .map(this::resolveRecipient)
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
@ -1500,7 +1508,7 @@ public class Manager implements Closeable {
break; break;
case QUIT: { case QUIT: {
if (groupV1 != null) { if (groupV1 != null) {
groupV1.removeMember(source); groupV1.removeMember(resolveRecipient(source));
account.getGroupStore().updateGroup(groupV1); account.getGroupStore().updateGroup(groupV1);
} }
break; break;
@ -1527,7 +1535,7 @@ public class Manager implements Closeable {
final var conversationPartnerAddress = isSync ? destination : source; final var conversationPartnerAddress = isSync ? destination : source;
if (conversationPartnerAddress != null && message.isEndSession()) { if (conversationPartnerAddress != null && message.isEndSession()) {
handleEndSession(conversationPartnerAddress); handleEndSession(resolveRecipient(conversationPartnerAddress));
} }
if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
@ -1613,7 +1621,7 @@ public class Manager implements Closeable {
final GroupInfoV2 groupInfoV2; final GroupInfoV2 groupInfoV2;
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
// Received a v2 group message for a v1 group, we need to locally migrate the group // Received a v2 group message for a v1 group, we need to locally migrate the group
account.getGroupStore().deleteGroup(groupInfo.getGroupId()); account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
logger.info("Locally migrated group {} to group v2, id: {}", logger.info("Locally migrated group {} to group v2, id: {}",
groupInfo.getGroupId().toBase64(), groupInfo.getGroupId().toBase64(),
@ -1641,7 +1649,7 @@ public class Manager implements Closeable {
downloadGroupAvatar(groupId, groupSecretParams, avatar); downloadGroupAvatar(groupId, groupSecretParams, avatar);
} }
} }
groupInfoV2.setGroup(group); groupInfoV2.setGroup(group, this::resolveRecipient);
account.getGroupStore().updateGroup(groupInfoV2); account.getGroupStore().updateGroup(groupInfoV2);
} }
@ -1885,7 +1893,7 @@ public class Manager implements Closeable {
} }
var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
var group = getGroup(groupId); var group = getGroup(groupId);
if (group != null && !group.isMember(source)) { if (group != null && !group.isMember(resolveRecipient(source))) {
return true; return true;
} }
} }
@ -1959,13 +1967,13 @@ public class Manager implements Closeable {
} }
syncGroup.addMembers(g.getMembers() syncGroup.addMembers(g.getMembers()
.stream() .stream()
.map(this::resolveSignalServiceAddress) .map(this::resolveRecipient)
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
if (!g.isActive()) { if (!g.isActive()) {
syncGroup.removeMember(account.getSelfAddress()); syncGroup.removeMember(account.getSelfRecipientId());
} else { } else {
// Add ourself to the member set as it's marked as active // Add ourself to the member set as it's marked as active
syncGroup.addMembers(List.of(account.getSelfAddress())); syncGroup.addMembers(List.of(account.getSelfRecipientId()));
} }
syncGroup.blocked = g.isBlocked(); syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) { if (g.getColor().isPresent()) {
@ -1975,7 +1983,6 @@ public class Manager implements Closeable {
if (g.getAvatar().isPresent()) { if (g.getAvatar().isPresent()) {
downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId()); downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
} }
syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived(); syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup); account.getGroupStore().updateGroup(syncGroup);
} }
@ -2282,13 +2289,16 @@ public class Manager implements Closeable {
var groupInfo = (GroupInfoV1) record; var groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name), Optional.fromNullable(groupInfo.name),
new ArrayList<>(groupInfo.getMembers()), groupInfo.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()),
createGroupAvatarAttachment(groupInfo.getGroupId()), createGroupAvatarAttachment(groupInfo.getGroupId()),
groupInfo.isMember(account.getSelfAddress()), groupInfo.isMember(account.getSelfRecipientId()),
Optional.of(groupInfo.messageExpirationTime), Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color), Optional.fromNullable(groupInfo.color),
groupInfo.blocked, groupInfo.blocked,
Optional.fromNullable(groupInfo.inboxPosition), Optional.absent(),
groupInfo.archived)); groupInfo.archived));
} }
} }
@ -2434,7 +2444,7 @@ public class Manager implements Closeable {
final var group = account.getGroupStore().getGroup(groupId); final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams)); ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
account.getGroupStore().updateGroup(group); account.getGroupStore().updateGroup(group);
} }
return group; return group;

View file

@ -1,5 +1,7 @@
package org.asamk.signal.manager.groups; package org.asamk.signal.manager.groups;
import java.util.Base64;
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes; import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
public class GroupIdV1 extends GroupId { public class GroupIdV1 extends GroupId {
@ -8,6 +10,10 @@ public class GroupIdV1 extends GroupId {
return new GroupIdV1(getSecretBytes(16)); return new GroupIdV1(getSecretBytes(16));
} }
public static GroupIdV1 fromBase64(String groupId) {
return new GroupIdV1(Base64.getDecoder().decode(groupId));
}
public GroupIdV1(final byte[] id) { public GroupIdV1(final byte[] id) {
super(id); super(id);
} }

View file

@ -100,7 +100,7 @@ public class GroupHelper {
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
} }
public GroupInfoV2 createGroupV2( public Pair<GroupInfoV2, DecryptedGroup> createGroupV2(
String name, Set<RecipientId> members, File avatarFile String name, Set<RecipientId> members, File avatarFile
) throws IOException { ) throws IOException {
final var avatarBytes = readAvatarBytes(avatarFile); final var avatarBytes = readAvatarBytes(avatarFile);
@ -129,9 +129,8 @@ public class GroupHelper {
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams); final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final var masterKey = groupSecretParams.getMasterKey(); final var masterKey = groupSecretParams.getMasterKey();
var g = new GroupInfoV2(groupId, masterKey); var g = new GroupInfoV2(groupId, masterKey);
g.setGroup(decryptedGroup);
return g; return new Pair<>(g, decryptedGroup);
} }
private byte[] readAvatarBytes(final File avatarFile) throws IOException { private byte[] readAvatarBytes(final File avatarFile) throws IOException {

View file

@ -6,7 +6,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -76,7 +75,7 @@ public class UnidentifiedAccessHelper {
} }
} }
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) { public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) {
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList()); return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
} }

View file

@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore; import org.asamk.signal.manager.storage.groups.GroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore; import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore; import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
@ -86,7 +86,8 @@ public class SignalAccount implements Closeable {
private SignedPreKeyStore signedPreKeyStore; private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore; private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore; private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore; private GroupStore groupStore;
private GroupStore.Storage groupStoreStorage;
private RecipientStore recipientStore; private RecipientStore recipientStore;
private StickerStore stickerStore; private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage; private StickerStore.Storage stickerStoreStorage;
@ -130,7 +131,9 @@ public class SignalAccount implements Closeable {
account.profileKey = profileKey; account.profileKey = profileKey;
account.initStores(dataPath, identityKey, registrationId); account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore); account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = false; account.registered = false;
@ -183,7 +186,9 @@ public class SignalAccount implements Closeable {
account.deviceId = deviceId; account.deviceId = deviceId;
account.initStores(dataPath, identityKey, registrationId); account.initStores(dataPath, identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore); account.stickerStore = new StickerStore(account::saveStickerStore);
account.registered = true; account.registered = true;
@ -209,6 +214,7 @@ public class SignalAccount implements Closeable {
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId); sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId); messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
} }
public static File getFileName(File dataPath, String username) { public static File getFileName(File dataPath, String username) {
@ -331,13 +337,16 @@ public class SignalAccount implements Closeable {
loadLegacyStores(rootNode, legacySignalProtocolStore); loadLegacyStores(rootNode, legacySignalProtocolStore);
var groupStoreNode = rootNode.get("groupStore"); if (rootNode.hasNonNull("groupStore")) {
if (groupStoreNode != null) { groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); groupStore = GroupStore.fromStorage(groupStoreStorage,
groupStore.groupCachePath = getGroupCachePath(dataPath, username); getGroupCachePath(dataPath, username),
} recipientStore::resolveRecipient,
if (groupStore == null) { this::saveGroupStore);
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); } else {
groupStore = new GroupStore(getGroupCachePath(dataPath, username),
recipientStore::resolveRecipient,
this::saveGroupStore);
} }
if (rootNode.hasNonNull("stickerStore")) { if (rootNode.hasNonNull("stickerStore")) {
@ -510,6 +519,11 @@ public class SignalAccount implements Closeable {
save(); save();
} }
private void saveGroupStore(GroupStore.Storage storage) {
this.groupStoreStorage = storage;
save();
}
public void save() { public void save() {
synchronized (fileChannel) { synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode(); var rootNode = jsonProcessor.createObjectNode();
@ -534,7 +548,7 @@ public class SignalAccount implements Closeable {
.put("nextSignedPreKeyId", nextSignedPreKeyId) .put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize())) .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered) .put("registered", registered)
.putPOJO("groupStore", groupStore) .putPOJO("groupStore", groupStoreStorage)
.putPOJO("stickerStore", stickerStoreStorage); .putPOJO("stickerStore", stickerStoreStorage);
try { try {
try (var output = new ByteArrayOutputStream()) { try (var output = new ByteArrayOutputStream()) {
@ -597,7 +611,7 @@ public class SignalAccount implements Closeable {
return identityKeyStore; return identityKeyStore;
} }
public JsonGroupStore getGroupStore() { public GroupStore getGroupStore() {
return groupStore; return groupStore;
} }

View file

@ -17,15 +17,15 @@ public class Utils {
} }
public static ObjectMapper createStorageObjectMapper() { public static ObjectMapper createStorageObjectMapper() {
final ObjectMapper jsonProcessor = new ObjectMapper(); final ObjectMapper objectMapper = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); objectMapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
return jsonProcessor; return objectMapper;
} }
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager.storage.groups; package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -12,66 +10,43 @@ import java.util.stream.Stream;
public abstract class GroupInfo { public abstract class GroupInfo {
@JsonIgnore
public abstract GroupId getGroupId(); public abstract GroupId getGroupId();
@JsonIgnore
public abstract String getTitle(); public abstract String getTitle();
@JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink(); public abstract GroupInviteLinkUrl getGroupInviteLink();
@JsonIgnore public abstract Set<RecipientId> getMembers();
public abstract Set<SignalServiceAddress> getMembers();
@JsonIgnore public Set<RecipientId> getPendingMembers() {
public Set<SignalServiceAddress> getPendingMembers() {
return Set.of(); return Set.of();
} }
@JsonIgnore public Set<RecipientId> getRequestingMembers() {
public Set<SignalServiceAddress> getRequestingMembers() {
return Set.of(); return Set.of();
} }
@JsonIgnore
public abstract boolean isBlocked(); public abstract boolean isBlocked();
@JsonIgnore
public abstract void setBlocked(boolean blocked); public abstract void setBlocked(boolean blocked);
@JsonIgnore
public abstract int getMessageExpirationTime(); public abstract int getMessageExpirationTime();
@JsonIgnore public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) { return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
} }
@JsonIgnore public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
return Stream.concat(getMembers().stream(), getPendingMembers().stream()) return Stream.concat(getMembers().stream(), getPendingMembers().stream())
.filter(member -> !member.matches(address)) .filter(member -> !member.equals(recipientId))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@JsonIgnore public boolean isMember(RecipientId recipientId) {
public boolean isMember(SignalServiceAddress address) { return getMembers().contains(recipientId);
for (var member : getMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
} }
@JsonIgnore public boolean isPendingMember(RecipientId recipientId) {
public boolean isPendingMember(SignalServiceAddress address) { return getPendingMembers().contains(recipientId);
for (var member : getPendingMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
} }
} }

View file

@ -1,55 +1,27 @@
package org.asamk.signal.manager.storage.groups; package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID;
public class GroupInfoV1 extends GroupInfo { public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
private final GroupIdV1 groupId; private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id; private GroupIdV2 expectedV2Id;
@JsonProperty
public String name; public String name;
@JsonProperty public Set<RecipientId> members = new HashSet<>();
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public String color; public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime; public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked; public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived; public boolean archived;
public GroupInfoV1(GroupIdV1 groupId) { public GroupInfoV1(GroupIdV1 groupId) {
@ -57,41 +29,30 @@ public class GroupInfoV1 extends GroupInfo {
} }
public GroupInfoV1( public GroupInfoV1(
@JsonProperty("groupId") byte[] groupId, final GroupIdV1 groupId,
@JsonProperty("expectedV2Id") byte[] expectedV2Id, final GroupIdV2 expectedV2Id,
@JsonProperty("name") String name, final String name,
@JsonProperty("members") Collection<SignalServiceAddress> members, final Set<RecipientId> members,
@JsonProperty("avatarId") long _ignored_avatarId, final String color,
@JsonProperty("color") String color, final int messageExpirationTime,
@JsonProperty("blocked") boolean blocked, final boolean blocked,
@JsonProperty("inboxPosition") Integer inboxPosition, final boolean archived
@JsonProperty("archived") boolean archived,
@JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active
) { ) {
this.groupId = GroupId.v1(groupId); this.groupId = groupId;
this.expectedV2Id = GroupId.v2(expectedV2Id); this.expectedV2Id = expectedV2Id;
this.name = name; this.name = name;
this.members.addAll(members); this.members = members;
this.color = color; this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime; this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
} }
@Override @Override
@JsonIgnore
public GroupIdV1 getGroupId() { public GroupIdV1 getGroupId() {
return groupId; return groupId;
} }
@JsonProperty("groupId")
private byte[] getGroupIdJackson() {
return groupId.serialize();
}
@JsonIgnore
public GroupIdV2 getExpectedV2Id() { public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) { if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId); expectedV2Id = GroupUtils.getGroupIdV2(groupId);
@ -99,11 +60,6 @@ public class GroupInfoV1 extends GroupInfo {
return expectedV2Id; return expectedV2Id;
} }
@JsonProperty("expectedV2Id")
private byte[] getExpectedV2IdJackson() {
return getExpectedV2Id().serialize();
}
@Override @Override
public String getTitle() { public String getTitle() {
return name; return name;
@ -114,8 +70,7 @@ public class GroupInfoV1 extends GroupInfo {
return null; return null;
} }
@JsonIgnore public Set<RecipientId> getMembers() {
public Set<SignalServiceAddress> getMembers() {
return members; return members;
} }
@ -134,79 +89,11 @@ public class GroupInfoV1 extends GroupInfo {
return messageExpirationTime; return messageExpirationTime;
} }
public void addMembers(Collection<SignalServiceAddress> addresses) { public void addMembers(Collection<RecipientId> members) {
for (var address : addresses) { this.members.addAll(members);
if (this.members.contains(address)) {
continue;
}
removeMember(address);
this.members.add(address);
}
} }
public void removeMember(SignalServiceAddress address) { public void removeMember(RecipientId recipientId) {
this.members.removeIf(member -> member.matches(address)); this.members.removeIf(member -> member.equals(recipientId));
}
private static final class JsonSignalServiceAddress {
@JsonProperty
private UUID uuid;
@JsonProperty
private String number;
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
this.uuid = uuid;
this.number = number;
}
JsonSignalServiceAddress(SignalServiceAddress address) {
this.uuid = address.getUuid().orNull();
this.number = address.getNumber().orNull();
}
SignalServiceAddress toSignalServiceAddress() {
return new SignalServiceAddress(uuid, number);
}
}
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(
final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeStartArray(value.size());
for (var address : value) {
if (address.getUuid().isPresent()) {
jgen.writeObject(new JsonSignalServiceAddress(address));
} else {
jgen.writeString(address.getNumber().get());
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var addresses = new HashSet<SignalServiceAddress>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
if (n.isTextual()) {
addresses.add(new SignalServiceAddress(null, n.textValue()));
} else {
var address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(address.toSignalServiceAddress());
}
}
return addresses;
}
} }
} }

View file

@ -2,6 +2,8 @@ package org.asamk.signal.manager.storage.groups;
import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupMasterKey;
@ -18,12 +20,19 @@ public class GroupInfoV2 extends GroupInfo {
private boolean blocked; private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
private RecipientResolver recipientResolver;
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId; this.groupId = groupId;
this.masterKey = masterKey; this.masterKey = masterKey;
} }
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
}
@Override @Override
public GroupIdV2 getGroupId() { public GroupIdV2 getGroupId() {
return groupId; return groupId;
@ -33,8 +42,9 @@ public class GroupInfoV2 extends GroupInfo {
return masterKey; return masterKey;
} }
public void setGroup(final DecryptedGroup group) { public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
this.group = group; this.group = group;
this.recipientResolver = recipientResolver;
} }
public DecryptedGroup getGroup() { public DecryptedGroup getGroup() {
@ -63,35 +73,38 @@ public class GroupInfoV2 extends GroupInfo {
} }
@Override @Override
public Set<SignalServiceAddress> getMembers() { public Set<RecipientId> getMembers() {
if (this.group == null) { if (this.group == null) {
return Set.of(); return Set.of();
} }
return group.getMembersList() return group.getMembersList()
.stream() .stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@Override @Override
public Set<SignalServiceAddress> getPendingMembers() { public Set<RecipientId> getPendingMembers() {
if (this.group == null) { if (this.group == null) {
return Set.of(); return Set.of();
} }
return group.getPendingMembersList() return group.getPendingMembersList()
.stream() .stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@Override @Override
public Set<SignalServiceAddress> getRequestingMembers() { public Set<RecipientId> getRequestingMembers() {
if (this.group == null) { if (this.group == null) {
return Set.of(); return Set.of();
} }
return group.getRequestingMembersList() return group.getRequestingMembersList()
.stream() .stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }

View file

@ -0,0 +1,474 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Hex;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupStore {
private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
private final File groupCachePath;
private final Map<GroupId, GroupInfo> groups;
private final RecipientResolver recipientResolver;
private final Saver saver;
private GroupStore(
final File groupCachePath,
final Map<GroupId, GroupInfo> groups,
final RecipientResolver recipientResolver,
final Saver saver
) {
this.groupCachePath = groupCachePath;
this.groups = groups;
this.recipientResolver = recipientResolver;
this.saver = saver;
}
public GroupStore(
final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
) {
this.groups = new HashMap<>();
this.groupCachePath = groupCachePath;
this.recipientResolver = recipientResolver;
this.saver = saver;
}
public static GroupStore fromStorage(
final Storage storage,
final File groupCachePath,
final RecipientResolver recipientResolver,
final Saver saver
) {
final var groups = storage.groups.stream().map(g -> {
if (g instanceof Storage.GroupV1) {
final var g1 = (Storage.GroupV1) g;
final var members = g1.members.stream().map(m -> {
if (m.recipientId == null) {
return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid),
m.number));
}
return RecipientId.of(m.recipientId);
}).collect(Collectors.toSet());
return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
g1.name,
members,
g1.color,
g1.messageExpirationTime,
g1.blocked,
g1.archived);
}
final var g2 = (Storage.GroupV2) g;
var groupId = GroupIdV2.fromBase64(g2.groupId);
GroupMasterKey masterKey;
try {
masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
} catch (InvalidInputException | IllegalArgumentException e) {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
return new GroupInfoV2(groupId, masterKey, g2.blocked);
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
}
public void updateGroup(GroupInfo group) {
final Storage storage;
synchronized (groups) {
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
storage = toStorageLocked();
}
saver.save(storage);
}
public void deleteGroupV1(GroupIdV1 groupId) {
final Storage storage;
synchronized (groups) {
groups.remove(groupId);
storage = toStorageLocked();
}
saver.save(storage);
}
public GroupInfo getGroup(GroupId groupId) {
synchronized (groups) {
return getGroupLocked(groupId);
}
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
synchronized (groups) {
var group = getGroupLocked(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
if (group == null) {
return new GroupInfoV1(groupId);
}
return null;
}
}
public List<GroupInfo> getGroups() {
synchronized (groups) {
final var groups = this.groups.values();
for (var group : groups) {
loadDecryptedGroupLocked(group);
}
return new ArrayList<>(groups);
}
}
public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
synchronized (groups) {
var modified = false;
for (var group : this.groups.values()) {
if (group instanceof GroupInfoV1) {
var groupV1 = (GroupInfoV1) group;
if (groupV1.isMember(toBeMergedRecipientId)) {
groupV1.removeMember(toBeMergedRecipientId);
groupV1.addMembers(List.of(recipientId));
modified = true;
}
}
}
if (modified) {
saver.save(toStorageLocked());
}
}
}
private GroupInfo getGroupLocked(final GroupId groupId) {
var group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = getGroupByV1IdLocked((GroupIdV1) groupId);
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
}
}
loadDecryptedGroupLocked(group);
return group;
}
private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
return groups.get(GroupUtils.getGroupIdV2(groupId));
}
private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
for (var g : groups.values()) {
if (g instanceof GroupInfoV1) {
final var gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
return null;
}
private void loadDecryptedGroupLocked(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
var groupFile = getGroupV2File(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupV2FileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (var stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
} catch (IOException ignored) {
}
}
}
private File getGroupV2FileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
private File getGroupV2File(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
private Storage toStorageLocked() {
return new Storage(groups.values().stream().map(g -> {
if (g instanceof GroupInfoV1) {
final var g1 = (GroupInfoV1) g;
return new Storage.GroupV1(g1.getGroupId().toBase64(),
g1.getExpectedV2Id().toBase64(),
g1.name,
g1.color,
g1.messageExpirationTime,
g1.blocked,
g1.archived,
g1.members.stream()
.map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
.collect(Collectors.toList()));
}
final var g2 = (GroupInfoV2) g;
return new Storage.GroupV2(g2.getGroupId().toBase64(),
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
g2.isBlocked());
}).collect(Collectors.toList()));
}
public static class Storage {
// @JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
public List<Storage.Group> groups;
// For deserialization
public Storage() {
}
public Storage(final List<Storage.Group> groups) {
this.groups = groups;
}
private abstract static class Group {
}
private static class GroupV1 extends Group {
public String groupId;
public String expectedV2Id;
public String name;
public String color;
public int messageExpirationTime;
public boolean blocked;
public boolean archived;
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public List<Member> members;
// For deserialization
public GroupV1() {
}
public GroupV1(
final String groupId,
final String expectedV2Id,
final String name,
final String color,
final int messageExpirationTime,
final boolean blocked,
final boolean archived,
final List<Member> members
) {
this.groupId = groupId;
this.expectedV2Id = expectedV2Id;
this.name = name;
this.color = color;
this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
this.members = members;
}
private static final class Member {
public Long recipientId;
public String uuid;
public String number;
Member(Long recipientId, final String uuid, final String number) {
this.recipientId = recipientId;
this.uuid = uuid;
this.number = number;
}
}
private static final class JsonSignalServiceAddress {
public String uuid;
public String number;
// For deserialization
public JsonSignalServiceAddress() {
}
JsonSignalServiceAddress(final String uuid, final String number) {
this.uuid = uuid;
this.number = number;
}
}
private static class MembersSerializer extends JsonSerializer<List<Member>> {
@Override
public void serialize(
final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeStartArray(value.size());
for (var address : value) {
if (address.recipientId != null) {
jgen.writeNumber(address.recipientId);
} else if (address.uuid != null) {
jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number));
} else {
jgen.writeString(address.number);
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
@Override
public List<Member> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var addresses = new ArrayList<Member>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
if (n.isTextual()) {
addresses.add(new Member(null, null, n.textValue()));
} else if (n.isNumber()) {
addresses.add(new Member(n.numberValue().longValue(), null, null));
} else {
var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(new Member(null, address.uuid, address.number));
}
}
return addresses;
}
}
}
private static class GroupV2 extends Group {
public String groupId;
public String masterKey;
public boolean blocked;
// For deserialization
private GroupV2() {
}
public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
}
}
}
// private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
//
// @Override
// public void serialize(
// final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
// ) throws IOException {
// jgen.writeStartArray(groups.size());
// for (var group : groups) {
// if (group instanceof GroupInfoV1) {
// jgen.writeObject(group);
// } else if (group instanceof GroupInfoV2) {
// final var groupV2 = (GroupInfoV2) group;
// jgen.writeStartObject();
// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
// jgen.writeStringField("masterKey",
// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
// jgen.writeBooleanField("blocked", groupV2.isBlocked());
// jgen.writeEndObject();
// } else {
// throw new AssertionError("Unknown group version");
// }
// }
// jgen.writeEndArray();
// }
// }
//
private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
@Override
public List<Storage.Group> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var groups = new ArrayList<Storage.Group>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
Storage.Group g;
if (n.hasNonNull("masterKey")) {
// a v2 group
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
} else {
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
}
groups.add(g);
}
return groups;
}
}
public interface Saver {
void save(Storage storage);
}
}

View file

@ -1,207 +0,0 @@
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Hex;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonGroupStore {
private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonIgnore
public File groupCachePath;
@JsonProperty("groups")
@JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
private JsonGroupStore() {
}
public JsonGroupStore(final File groupCachePath) {
this.groupCachePath = groupCachePath;
}
public void updateGroup(GroupInfo group) {
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
}
public void deleteGroup(GroupId groupId) {
groups.remove(groupId);
}
public GroupInfo getGroup(GroupId groupId) {
var group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2Id((GroupIdV2) groupId);
}
}
loadDecryptedGroup(group);
return group;
}
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
for (var g : groups.values()) {
if (g instanceof GroupInfoV1) {
final var gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
return null;
}
private void loadDecryptedGroup(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
var groupFile = getGroupFile(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupFileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (var stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
} catch (IOException ignored) {
}
}
}
private File getGroupFileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
private File getGroupFile(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
var group = getGroup(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
if (group == null) {
return new GroupInfoV1(groupId);
}
return null;
}
@JsonIgnore
public List<GroupInfo> getGroups() {
final var groups = this.groups.values();
for (var group : groups) {
loadDecryptedGroup(group);
}
return new ArrayList<>(groups);
}
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
@Override
public void serialize(
final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
final var groups = value.values();
jgen.writeStartArray(groups.size());
for (var group : groups) {
if (group instanceof GroupInfoV1) {
jgen.writeObject(group);
} else if (group instanceof GroupInfoV2) {
final var groupV2 = (GroupInfoV2) group;
jgen.writeStartObject();
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
jgen.writeStringField("masterKey",
Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
jgen.writeBooleanField("blocked", groupV2.isBlocked());
jgen.writeEndObject();
} else {
throw new AssertionError("Unknown group version");
}
}
jgen.writeEndArray();
}
}
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
@Override
public Map<GroupId, GroupInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
var groups = new HashMap<GroupId, GroupInfo>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (var n : node) {
GroupInfo g;
if (n.hasNonNull("masterKey")) {
// a v2 group
var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
try {
var masterKey = new GroupMasterKey(Base64.getDecoder().decode(n.get("masterKey").asText()));
g = new GroupInfoV2(groupId, masterKey);
} catch (InvalidInputException | IllegalArgumentException e) {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
g.setBlocked(n.get("blocked").asBoolean(false));
} else {
g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
}
groups.put(g.getGroupId(), g);
}
return groups;
}
}
}

View file

@ -46,7 +46,7 @@ public class JoinGroupCommand implements LocalCommand {
final var results = m.joinGroup(linkUrl); final var results = m.joinGroup(linkUrl);
var newGroupId = results.first(); var newGroupId = results.first();
if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
writer.println("Requested to join group \"{}\"", newGroupId.toBase64()); writer.println("Requested to join group \"{}\"", newGroupId.toBase64());
} else { } else {
writer.println("Joined group \"{}\"", newGroupId.toBase64()); writer.println("Joined group \"{}\"", newGroupId.toBase64());

View file

@ -11,6 +11,7 @@ import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -23,7 +24,7 @@ public class ListGroupsCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class); private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class);
private static Set<String> resolveMembers(Manager m, Set<SignalServiceAddress> addresses) { private static Set<String> resolveMembers(Manager m, Set<RecipientId> addresses) {
return addresses.stream() return addresses.stream()
.map(m::resolveSignalServiceAddress) .map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier) .map(SignalServiceAddress::getLegacyIdentifier)
@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand {
"Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}", "Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}",
group.getGroupId().toBase64(), group.getGroupId().toBase64(),
group.getTitle(), group.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfRecipientId()),
group.isBlocked(), group.isBlocked(),
resolveMembers(m, group.getMembers()), resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()), resolveMembers(m, group.getPendingMembers()),
@ -50,7 +51,7 @@ public class ListGroupsCommand implements LocalCommand {
writer.println("Id: {} Name: {} Active: {} Blocked: {}", writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getGroupId().toBase64(), group.getGroupId().toBase64(),
group.getTitle(), group.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfRecipientId()),
group.isBlocked()); group.isBlocked());
} }
} }
@ -80,7 +81,7 @@ public class ListGroupsCommand implements LocalCommand {
jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(), jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(),
group.getTitle(), group.getTitle(),
group.isMember(m.getSelfAddress()), group.isMember(m.getSelfRecipientId()),
group.isBlocked(), group.isBlocked(),
resolveMembers(m, group.getMembers()), resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()), resolveMembers(m, group.getPendingMembers()),

View file

@ -472,7 +472,7 @@ public class DbusSignalImpl implements Signal {
if (group == null) { if (group == null) {
return false; return false;
} else { } else {
return group.isMember(m.getSelfAddress()); return group.isMember(m.getSelfRecipientId());
} }
} }
} }