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" } ]
This commit is contained in:
AsamK 2020-03-23 19:08:41 +01:00
parent a4e1d69788
commit f982d2752e
5 changed files with 159 additions and 61 deletions

View file

@ -19,7 +19,7 @@ public interface Signal extends DBusInterface {
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException;
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, InvalidNumberException; void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
String getContactName(String number) throws InvalidNumberException; String getContactName(String number) throws InvalidNumberException;

View file

@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
import java.util.List; import java.util.List;
public class ListGroupsCommand implements LocalCommand { 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) { if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", 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 { } else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", 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"); boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) { for (GroupInfo group : groups) {
printGroup(group, detailed, m.getUsername()); printGroup(group, detailed, m.getSelfAddress());
} }
return 0; return 0;
} }

View file

@ -9,7 +9,6 @@ import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; 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.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException; import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class QuitGroupCommand implements LocalCommand { public class QuitGroupCommand implements LocalCommand {
@ -58,9 +56,6 @@ public class QuitGroupCommand implements LocalCommand {
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
} }
} }
} }

View file

@ -171,6 +171,10 @@ public class Manager implements Signal {
return username; return username;
} }
public SignalServiceAddress getSelfAddress() {
return account.getSelfAddress();
}
private SignalServiceAccountManager getSignalServiceAccountManager() { private SignalServiceAccountManager getSignalServiceAccountManager() {
return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); 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) { if (g == null) {
throw new GroupNotFoundException(groupId); throw new GroupNotFoundException(groupId);
} }
for (String member : g.members) { if (!g.isMember(account.getSelfAddress())) {
if (member.equals(account.getUsername())) { throw new NotAGroupMemberException(groupId, g.name);
return g;
}
} }
throw new NotAGroupMemberException(groupId, g.name); return g;
} }
public List<GroupInfo> getGroups() { public List<GroupInfo> getGroups() {
@ -514,7 +516,7 @@ public class Manager implements Signal {
@Override @Override
public void sendGroupMessage(String messageText, List<String> attachments, public void sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId) byte[] groupId)
throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) { if (attachments != null) {
messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
@ -532,15 +534,12 @@ public class Manager implements Signal {
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members); sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
// Don't send group message to ourself
membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
} }
public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor,
long targetSentTimestamp, byte[] groupId) long targetSentTimestamp, byte[] groupId)
throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction) .withReaction(reaction)
@ -552,13 +551,10 @@ public class Manager implements Signal {
messageBuilder.asGroupMessage(group); messageBuilder.asGroupMessage(group);
} }
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members); sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
// Don't send group message to ourself
membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
} }
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) SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
.withId(groupId) .withId(groupId)
.build(); .build();
@ -567,18 +563,18 @@ public class Manager implements Signal {
.asGroupMessage(group); .asGroupMessage(group);
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
g.members.remove(account.getUsername()); g.removeMember(account.getSelfAddress());
account.getGroupStore().updateGroup(g); account.getGroupStore().updateGroup(g);
sendMessageLegacy(messageBuilder, getSignalServiceAddresses(g.members)); sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
} }
private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
GroupInfo g; GroupInfo g;
if (groupId == null) { if (groupId == null) {
// Create new group // Create new group
g = new GroupInfo(KeyUtils.createGroupId()); g = new GroupInfo(KeyUtils.createGroupId());
g.members.add(account.getUsername()); g.addMembers(Collections.singleton(account.getSelfAddress()));
} else { } else {
g = getGroupForSending(groupId); g = getGroupForSending(groupId);
} }
@ -588,25 +584,26 @@ public class Manager implements Signal {
} }
if (members != null) { if (members != null) {
Set<String> newMembers = new HashSet<>(); final Set<String> newE164Members = new HashSet<>();
for (String member : members) { for (SignalServiceAddress member : members) {
member = Utils.canonicalizeNumber(member, account.getUsername()); if (g.isMember(member) || !member.getNumber().isPresent()) {
if (g.members.contains(member)) {
continue; continue;
} }
newMembers.add(member); newE164Members.add(member.getNumber().get());
g.members.add(member);
} }
final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
if (contacts.size() != newMembers.size()) { final List<ContactTokenDetails> contacts = accountManager.getContacts(newE164Members);
if (contacts.size() != newE164Members.size()) {
// Some of the new members are not registered on Signal // Some of the new members are not registered on Signal
for (ContactTokenDetails contact : contacts) { 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.err.println("Aborting…");
System.exit(1); System.exit(1);
} }
g.addMembers(members);
} }
if (avatarFile != null) { if (avatarFile != null) {
@ -619,10 +616,7 @@ public class Manager implements Signal {
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
final Collection<SignalServiceAddress> membersSend = getSignalServiceAddresses(g.members); sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
// Don't send group message to ourself
membersSend.remove(account.getSelfAddress());
sendMessageLegacy(messageBuilder, membersSend);
return g.groupId; return g.groupId;
} }
@ -632,7 +626,7 @@ public class Manager implements Signal {
} }
GroupInfo g = getGroupForSending(groupId); GroupInfo g = getGroupForSending(groupId);
if (!g.members.contains(recipient.getNumber().get())) { if (!g.isMember(recipient)) {
return; return;
} }
@ -819,9 +813,9 @@ public class Manager implements Signal {
public List<String> getGroupMembers(byte[] groupId) { public List<String> getGroupMembers(byte[] groupId) {
GroupInfo group = getGroup(groupId); GroupInfo group = getGroup(groupId);
if (group == null) { if (group == null) {
return new ArrayList<>(); return Collections.emptyList();
} else { } else {
return new ArrayList<>(group.members); return new ArrayList<>(group.getMembersE164());
} }
} }
@ -839,7 +833,7 @@ public class Manager implements Signal {
if (avatar.isEmpty()) { if (avatar.isEmpty()) {
avatar = null; 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(); e.printStackTrace();
} }
} else { } else {
group.members.remove(source.getNumber().get()); group.removeMember(source);
account.getGroupStore().updateGroup(group); account.getGroupStore().updateGroup(group);
} }
break; break;
@ -1559,10 +1553,10 @@ public class Manager implements Signal {
} }
syncGroup.addMembers(g.getMembers()); syncGroup.addMembers(g.getMembers());
if (!g.isActive()) { if (!g.isActive()) {
syncGroup.members.remove(account.getUsername()); syncGroup.removeMember(account.getSelfAddress());
} 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.members.add(account.getUsername()); syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
} }
syncGroup.blocked = g.isBlocked(); syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) { if (g.getColor().isPresent()) {
@ -1778,7 +1772,7 @@ public class Manager implements Signal {
ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId));
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), 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)); Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived));
} }
} }

View file

@ -2,15 +2,29 @@ package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; 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 org.whispersystems.signalservice.api.push.SignalServiceAddress;
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 GroupInfo { public class GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty @JsonProperty
public final byte[] groupId; public final byte[] groupId;
@ -18,7 +32,9 @@ public class GroupInfo {
public String name; public String name;
@JsonProperty @JsonProperty
public Set<String> members = new HashSet<>(); @JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty @JsonProperty
public String color; public String color;
@JsonProperty(defaultValue = "false") @JsonProperty(defaultValue = "false")
@ -38,7 +54,7 @@ public class GroupInfo {
this.groupId = groupId; this.groupId = groupId;
} }
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> 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<SignalServiceAddress> 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.groupId = groupId;
this.name = name; this.name = name;
this.members.addAll(members); this.members.addAll(members);
@ -56,16 +72,108 @@ public class GroupInfo {
@JsonIgnore @JsonIgnore
public Set<SignalServiceAddress> getMembers() { public Set<SignalServiceAddress> getMembers() {
Set<SignalServiceAddress> addresses = new HashSet<>(members.size()); return members;
for (String member : members) {
addresses.add(new SignalServiceAddress(null, member));
}
return addresses;
} }
public void addMembers(Collection<SignalServiceAddress> members) { @JsonIgnore
public Set<String> getMembersE164() {
Set<String> membersE164 = new HashSet<>();
for (SignalServiceAddress member : members) { 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<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
Set<SignalServiceAddress> members = new HashSet<>(this.members.size());
for (SignalServiceAddress member : this.members) {
if (!member.matches(address)) {
members.add(member);
}
}
return members;
}
public void addMembers(Collection<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public void serialize(final Set<SignalServiceAddress> 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<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Set<SignalServiceAddress> 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;
} }
} }
} }