mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Refactor group store
This commit is contained in:
parent
a1014ba39c
commit
5b8c0c4e2d
14 changed files with 644 additions and 473 deletions
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue