From: AsamK Date: Mon, 23 Mar 2020 18:08:41 +0000 (+0100) Subject: Store group member uuids in group store X-Git-Tag: v0.6.6~16 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/f982d2752e7531ea69296e09309715fdff9e0b2c?ds=sidebyside Store group member uuids in group store The member list is now stored as a mixed list of strings and objects, e.g.: "members": [ "+XXXX", { "number": "+XXXX", "uuid": "XXX-XX" } ] --- diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 90d02758..528116b1 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -19,7 +19,7 @@ public interface Signal extends DBusInterface { void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, InvalidNumberException; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; String getContactName(String number) throws InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 565bacba..0baa8744 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; import java.util.List; public class ListGroupsCommand implements LocalCommand { - private static void printGroup(GroupInfo group, boolean detailed, String username) { + private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) { if (detailed) { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", - Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked, group.members)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked)); } } @@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand { boolean detailed = ns.getBoolean("detailed"); for (GroupInfo group : groups) { - printGroup(group, detailed, m.getUsername()); + printGroup(group, detailed, m.getSelfAddress()); } return 0; } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 38bc79ac..6e53cb2a 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -9,7 +9,6 @@ import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -18,7 +17,6 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class QuitGroupCommand implements LocalCommand { @@ -58,9 +56,6 @@ public class QuitGroupCommand implements LocalCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; - } catch (InvalidNumberException e) { - handleInvalidNumberException(e); - return 1; } } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8364888c..7a28f7bc 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -171,6 +171,10 @@ public class Manager implements Signal { return username; } + public SignalServiceAddress getSelfAddress() { + return account.getSelfAddress(); + } + private SignalServiceAccountManager getSignalServiceAccountManager() { return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); } @@ -499,12 +503,10 @@ public class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - for (String member : g.members) { - if (member.equals(account.getUsername())) { - return g; - } + if (!g.isMember(account.getSelfAddress())) { + throw new NotAGroupMemberException(groupId, g.name); } - throw new NotAGroupMemberException(groupId, g.name); + return g; } public List getGroups() { @@ -514,7 +516,7 @@ public class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); @@ -532,15 +534,12 @@ public class Manager implements Signal { final GroupInfo g = getGroupForSending(groupId); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) @@ -552,13 +551,10 @@ public class Manager implements Signal { messageBuilder.asGroupMessage(group); } final GroupInfo g = getGroupForSending(groupId); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, InvalidNumberException { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -567,18 +563,18 @@ public class Manager implements Signal { .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); - g.members.remove(account.getUsername()); + g.removeMember(account.getSelfAddress()); account.getGroupStore().updateGroup(g); - sendMessageLegacy(messageBuilder, getSignalServiceAddresses(g.members)); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group g = new GroupInfo(KeyUtils.createGroupId()); - g.members.add(account.getUsername()); + g.addMembers(Collections.singleton(account.getSelfAddress())); } else { g = getGroupForSending(groupId); } @@ -588,25 +584,26 @@ public class Manager implements Signal { } if (members != null) { - Set newMembers = new HashSet<>(); - for (String member : members) { - member = Utils.canonicalizeNumber(member, account.getUsername()); - if (g.members.contains(member)) { + final Set newE164Members = new HashSet<>(); + for (SignalServiceAddress member : members) { + if (g.isMember(member) || !member.getNumber().isPresent()) { continue; } - newMembers.add(member); - g.members.add(member); + newE164Members.add(member.getNumber().get()); } - final List contacts = accountManager.getContacts(newMembers); - if (contacts.size() != newMembers.size()) { + + final List contacts = accountManager.getContacts(newE164Members); + if (contacts.size() != newE164Members.size()) { // Some of the new members are not registered on Signal for (ContactTokenDetails contact : contacts) { - newMembers.remove(contact.getNumber()); + newE164Members.remove(contact.getNumber()); } - System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal"); + System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal"); System.err.println("Aborting…"); System.exit(1); } + + g.addMembers(members); } if (avatarFile != null) { @@ -619,10 +616,7 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return g.groupId; } @@ -632,7 +626,7 @@ public class Manager implements Signal { } GroupInfo g = getGroupForSending(groupId); - if (!g.members.contains(recipient.getNumber().get())) { + if (!g.isMember(recipient)) { return; } @@ -819,9 +813,9 @@ public class Manager implements Signal { public List getGroupMembers(byte[] groupId) { GroupInfo group = getGroup(groupId); if (group == null) { - return new ArrayList<>(); + return Collections.emptyList(); } else { - return new ArrayList<>(group.members); + return new ArrayList<>(group.getMembersE164()); } } @@ -839,7 +833,7 @@ public class Manager implements Signal { if (avatar.isEmpty()) { avatar = null; } - return sendUpdateGroupMessage(groupId, name, members, avatar); + return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar); } /** @@ -1284,7 +1278,7 @@ public class Manager implements Signal { e.printStackTrace(); } } else { - group.members.remove(source.getNumber().get()); + group.removeMember(source); account.getGroupStore().updateGroup(group); } break; @@ -1559,10 +1553,10 @@ public class Manager implements Signal { } syncGroup.addMembers(g.getMembers()); if (!g.isActive()) { - syncGroup.members.remove(account.getUsername()); + syncGroup.removeMember(account.getSelfAddress()); } else { // Add ourself to the member set as it's marked as active - syncGroup.members.add(account.getUsername()); + syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -1778,7 +1772,7 @@ public class Manager implements Signal { ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.members.contains(account.getUsername()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), + record.isMember(account.getSelfAddress()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index cb53d3af..21ba910f 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -2,15 +2,29 @@ package org.asamk.signal.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.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.UUID; public class GroupInfo { + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + @JsonProperty public final byte[] groupId; @@ -18,7 +32,9 @@ public class GroupInfo { public String name; @JsonProperty - public Set members = new HashSet<>(); + @JsonDeserialize(using = MembersDeserializer.class) + @JsonSerialize(using = MembersSerializer.class) + public Set members = new HashSet<>(); @JsonProperty public String color; @JsonProperty(defaultValue = "false") @@ -38,7 +54,7 @@ public class GroupInfo { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { this.groupId = groupId; this.name = name; this.members.addAll(members); @@ -56,16 +72,108 @@ public class GroupInfo { @JsonIgnore public Set getMembers() { - Set addresses = new HashSet<>(members.size()); - for (String member : members) { - addresses.add(new SignalServiceAddress(null, member)); - } - return addresses; + return members; } - public void addMembers(Collection members) { + @JsonIgnore + public Set getMembersE164() { + Set membersE164 = new HashSet<>(); for (SignalServiceAddress member : members) { - this.members.add(member.getNumber().get()); + if (!member.getNumber().isPresent()) { + continue; + } + membersE164.add(member.getNumber().get()); + } + return membersE164; + } + + @JsonIgnore + public Set getMembersWithout(SignalServiceAddress address) { + Set members = new HashSet<>(this.members.size()); + for (SignalServiceAddress member : this.members) { + if (!member.matches(address)) { + members.add(member); + } + } + return members; + } + + public void addMembers(Collection addresses) { + for (SignalServiceAddress address : addresses) { + removeMember(address); + this.members.add(address); + } + } + + public void removeMember(SignalServiceAddress address) { + this.members.removeIf(member -> member.matches(address)); + } + + @JsonIgnore + public boolean isMember(SignalServiceAddress address) { + for (SignalServiceAddress member : this.members) { + if (member.matches(address)) { + return true; + } + } + return false; + } + + 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> { + + @Override + public void serialize(final Set value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeStartArray(value.size()); + for (SignalServiceAddress 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> { + + @Override + public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Set addresses = new HashSet<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + if (n.isTextual()) { + addresses.add(new SignalServiceAddress(null, n.textValue())); + } else { + JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class); + addresses.add(address.toSignalServiceAddress()); + } + } + + return addresses; } } }