return account.getSelfAddress();
}
+ public RecipientId getSelfRecipientId() {
+ return account.getSelfRecipientId();
+ }
+
private IdentityKeyPair getIdentityKeyPair() {
return account.getIdentityKeyPair();
}
if (g == null) {
throw new GroupNotFoundException(groupId);
}
- if (!g.isMember(account.getSelfAddress())) {
+ if (!g.isMember(account.getSelfRecipientId())) {
throw new NotAGroupMemberException(groupId, g.getTitle());
}
return g;
if (g == null) {
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());
}
return g;
GroupUtils.setGroupContext(messageBuilder, g);
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 {
var groupInfoV1 = (GroupInfoV1) g;
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build();
messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
- groupInfoV1.removeMember(account.getSelfAddress());
+ groupInfoV1.removeMember(account.getSelfRecipientId());
account.getGroupStore().updateGroup(groupInfoV1);
} else {
final var groupInfoV2 = (GroupInfoV2) g;
final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
- groupInfoV2.setGroup(groupGroupChangePair.first());
+ groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
account.getGroupStore().updateGroup(groupInfoV2);
}
- return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+ return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
}
public Pair<GroupId, List<SendMessageResult>> updateGroup(
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId,
name,
- members == null
- ? null
- : getSignalServiceAddresses(members).stream()
- .map(this::resolveRecipient)
- .collect(Collectors.toSet()),
+ members == null ? null : getSignalServiceAddresses(members),
avatarFile);
}
SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
- var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
+ var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
members == null ? Set.of() : members,
avatarFile);
- if (gv2 == null) {
+ if (gv2Pair == null) {
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
- gv1.addMembers(List.of(account.getSelfAddress()));
+ gv1.addMembers(List.of(account.getSelfRecipientId()));
updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
} else {
+ final var gv2 = gv2Pair.first();
+ final var decryptedGroup = gv2Pair.second();
+
+ gv2.setGroup(decryptedGroup, this::resolveRecipient);
if (avatarFile != null) {
avatarStore.storeGroupAvatar(gv2.getGroupId(),
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
final var groupInfoV2 = (GroupInfoV2) group;
Pair<Long, List<SendMessageResult>> result = null;
- if (groupInfoV2.isPendingMember(getSelfAddress())) {
+ if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
result = sendUpdateGroupMessage(groupInfoV2,
groupGroupChangePair.first(),
if (members != null) {
final var newMembers = new HashSet<>(members);
- newMembers.removeAll(group.getMembers()
- .stream()
- .map(this::resolveRecipient)
- .collect(Collectors.toSet()));
+ newMembers.removeAll(group.getMembers());
if (newMembers.size() > 0) {
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
result = sendUpdateGroupMessage(groupInfoV2,
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());
}
}
if (members != null) {
- final var memberAddresses = members.stream()
+ final var newMemberAddresses = members.stream()
+ .filter(member -> !g.isMember(member))
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList());
final var newE164Members = new HashSet<String>();
- for (var member : memberAddresses) {
- if (g.isMember(member) || !member.getNumber().isPresent()) {
+ for (var member : newMemberAddresses) {
+ if (!member.getNumber().isPresent()) {
continue;
}
newE164Members.add(member.getNumber().get());
+ " to group: Not registered on Signal");
}
- g.addMembers(memberAddresses);
+ g.addMembers(members);
}
if (avatarFile != null) {
private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
) throws IOException {
- group.setGroup(newDecryptedGroup);
+ group.setGroup(newDecryptedGroup, this::resolveRecipient);
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
account.getGroupStore().updateGroup(group);
- return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
}
Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
}
g = (GroupInfoV1) group;
- if (!g.isMember(recipient)) {
+ if (!g.isMember(resolveRecipient(recipient))) {
throw new NotAGroupMemberException(groupId, g.name);
}
var messageBuilder = getGroupUpdateMessageBuilder(g);
// 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 {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.getGroupId().serialize())
.withName(g.name)
- .withMembers(new ArrayList<>(g.getMembers()));
+ .withMembers(g.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toList()));
try {
final var attachment = createGroupAvatarAttachment(g.getGroupId());
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
// 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(
.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();
- sendMessage(messageBuilder, List.of(address));
+ sendMessage(messageBuilder, Set.of(recipientId));
}
/**
) throws IOException, InvalidNumberException {
var recipientId = canonicalizeAndResolveRecipient(number);
setExpirationTimer(recipientId, messageExpirationTimer);
- sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
+ sendExpirationTimerUpdate(recipientId);
account.save();
}
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 addressesMissingUuid = new HashSet<SignalServiceAddress>();
}
}
- return signalServiceAddresses;
+ return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
}
private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
}
private Pair<Long, List<SendMessageResult>> sendMessage(
- SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
+ SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
) 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();
messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe();
try {
var messageSender = createMessageSender();
final var isRecipientUpdate = false;
- var result = messageSender.sendMessage(new ArrayList<>(recipients),
- unidentifiedAccessHelper.getAccessFor(recipientIds),
+ final var recipientIdList = new ArrayList<>(recipientIds);
+ final var addresses = recipientIdList.stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toList());
+ var result = messageSender.sendMessage(addresses,
+ unidentifiedAccessHelper.getAccessFor(recipientIdList),
isRecipientUpdate,
message);
} else {
// Send to all individually, so sync messages are sent correctly
messageBuilder.withProfileKey(account.getProfileKey().serialize());
- var results = new ArrayList<SendMessageResult>(recipients.size());
- for (var address : recipients) {
- final var contact = account.getContactStore().getContact(resolveRecipient(address));
+ var results = new ArrayList<SendMessageResult>(recipientIds.size());
+ for (var recipientId : recipientIds) {
+ final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime);
message = messageBuilder.build();
- results.add(sendMessage(address, message));
+ results.add(sendMessage(resolveSignalServiceAddress(recipientId), message));
}
return new Pair<>(timestamp, results);
}
} finally {
if (message != null && message.isEndSession()) {
- for (var recipient : recipients) {
+ for (var recipient : recipientIds) {
handleEndSession(recipient);
}
}
}
}
- private void handleEndSession(SignalServiceAddress source) {
- account.getSessionStore().deleteAllSessions(source.getIdentifier());
+ private void handleEndSession(RecipientId recipientId) {
+ account.getSessionStore().deleteAllSessions(recipientId);
}
private List<HandleAction> handleSignalServiceDataMessage(
groupV1.addMembers(groupInfo.getMembers()
.get()
.stream()
- .map(this::resolveSignalServiceAddress)
+ .map(this::resolveRecipient)
.collect(Collectors.toSet()));
}
break;
case QUIT: {
if (groupV1 != null) {
- groupV1.removeMember(source);
+ groupV1.removeMember(resolveRecipient(source));
account.getGroupStore().updateGroup(groupV1);
}
break;
final var conversationPartnerAddress = isSync ? destination : source;
if (conversationPartnerAddress != null && message.isEndSession()) {
- handleEndSession(conversationPartnerAddress);
+ handleEndSession(resolveRecipient(conversationPartnerAddress));
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) {
final GroupInfoV2 groupInfoV2;
if (groupInfo instanceof GroupInfoV1) {
// 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);
logger.info("Locally migrated group {} to group v2, id: {}",
groupInfo.getGroupId().toBase64(),
downloadGroupAvatar(groupId, groupSecretParams, avatar);
}
}
- groupInfoV2.setGroup(group);
+ groupInfoV2.setGroup(group, this::resolveRecipient);
account.getGroupStore().updateGroup(groupInfoV2);
}
}
var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
var group = getGroup(groupId);
- if (group != null && !group.isMember(source)) {
+ if (group != null && !group.isMember(resolveRecipient(source))) {
return true;
}
}
}
syncGroup.addMembers(g.getMembers()
.stream()
- .map(this::resolveSignalServiceAddress)
+ .map(this::resolveRecipient)
.collect(Collectors.toSet()));
if (!g.isActive()) {
- syncGroup.removeMember(account.getSelfAddress());
+ syncGroup.removeMember(account.getSelfRecipientId());
} else {
// 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();
if (g.getColor().isPresent()) {
if (g.getAvatar().isPresent()) {
downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
}
- syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup);
}
var groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name),
- new ArrayList<>(groupInfo.getMembers()),
+ groupInfo.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toList()),
createGroupAvatarAttachment(groupInfo.getGroupId()),
- groupInfo.isMember(account.getSelfAddress()),
+ groupInfo.isMember(account.getSelfRecipientId()),
Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color),
groupInfo.blocked,
- Optional.fromNullable(groupInfo.inboxPosition),
+ Optional.absent(),
groupInfo.archived));
}
}
final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
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);
}
return group;
package org.asamk.signal.manager.groups;
+import java.util.Base64;
+
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
public class GroupIdV1 extends GroupId {
return new GroupIdV1(getSecretBytes(16));
}
+ public static GroupIdV1 fromBase64(String groupId) {
+ return new GroupIdV1(Base64.getDecoder().decode(groupId));
+ }
+
public GroupIdV1(final byte[] id) {
super(id);
}
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
}
- public GroupInfoV2 createGroupV2(
+ public Pair<GroupInfoV2, DecryptedGroup> createGroupV2(
String name, Set<RecipientId> members, File avatarFile
) throws IOException {
final var avatarBytes = readAvatarBytes(avatarFile);
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final var masterKey = groupSecretParams.getMasterKey();
var g = new GroupInfoV2(groupId, masterKey);
- g.setGroup(decryptedGroup);
- return g;
+ return new Pair<>(g, decryptedGroup);
}
private byte[] readAvatarBytes(final File avatarFile) throws IOException {
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
-import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
}
}
- 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());
}
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
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.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
private SignedPreKeyStore signedPreKeyStore;
private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore;
- private JsonGroupStore groupStore;
+ private GroupStore groupStore;
+ private GroupStore.Storage groupStoreStorage;
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
account.profileKey = profileKey;
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.registered = false;
account.deviceId = deviceId;
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.registered = true;
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
+ groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
}
public static File getFileName(File dataPath, String username) {
loadLegacyStores(rootNode, legacySignalProtocolStore);
- var groupStoreNode = rootNode.get("groupStore");
- if (groupStoreNode != null) {
- groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
- groupStore.groupCachePath = getGroupCachePath(dataPath, username);
- }
- if (groupStore == null) {
- groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
+ if (rootNode.hasNonNull("groupStore")) {
+ groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
+ groupStore = GroupStore.fromStorage(groupStoreStorage,
+ getGroupCachePath(dataPath, username),
+ recipientStore::resolveRecipient,
+ this::saveGroupStore);
+ } else {
+ groupStore = new GroupStore(getGroupCachePath(dataPath, username),
+ recipientStore::resolveRecipient,
+ this::saveGroupStore);
}
if (rootNode.hasNonNull("stickerStore")) {
save();
}
+ private void saveGroupStore(GroupStore.Storage storage) {
+ this.groupStoreStorage = storage;
+ save();
+ }
+
public void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
- .putPOJO("groupStore", groupStore)
+ .putPOJO("groupStore", groupStoreStorage)
.putPOJO("stickerStore", stickerStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
return identityKeyStore;
}
- public JsonGroupStore getGroupStore() {
+ public GroupStore getGroupStore() {
return groupStore;
}
}
public static ObjectMapper createStorageObjectMapper() {
- final ObjectMapper jsonProcessor = new ObjectMapper();
+ final ObjectMapper objectMapper = new ObjectMapper();
- jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
- jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
- jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
- jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
- jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
+ objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
+ objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ objectMapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
+ objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
- return jsonProcessor;
+ return objectMapper;
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
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.GroupInviteLinkUrl;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Set;
import java.util.stream.Collectors;
public abstract class GroupInfo {
- @JsonIgnore
public abstract GroupId getGroupId();
- @JsonIgnore
public abstract String getTitle();
- @JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink();
- @JsonIgnore
- public abstract Set<SignalServiceAddress> getMembers();
+ public abstract Set<RecipientId> getMembers();
- @JsonIgnore
- public Set<SignalServiceAddress> getPendingMembers() {
+ public Set<RecipientId> getPendingMembers() {
return Set.of();
}
- @JsonIgnore
- public Set<SignalServiceAddress> getRequestingMembers() {
+ public Set<RecipientId> getRequestingMembers() {
return Set.of();
}
- @JsonIgnore
public abstract boolean isBlocked();
- @JsonIgnore
public abstract void setBlocked(boolean blocked);
- @JsonIgnore
public abstract int getMessageExpirationTime();
- @JsonIgnore
- public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
- return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
+ public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
+ return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
}
- @JsonIgnore
- public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
+ public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
- .filter(member -> !member.matches(address))
+ .filter(member -> !member.equals(recipientId))
.collect(Collectors.toSet());
}
- @JsonIgnore
- public boolean isMember(SignalServiceAddress address) {
- for (var member : getMembers()) {
- if (member.matches(address)) {
- return true;
- }
- }
- return false;
+ public boolean isMember(RecipientId recipientId) {
+ return getMembers().contains(recipientId);
}
- @JsonIgnore
- public boolean isPendingMember(SignalServiceAddress address) {
- for (var member : getPendingMembers()) {
- if (member.matches(address)) {
- return true;
- }
- }
- return false;
+ public boolean isPendingMember(RecipientId recipientId) {
+ return getPendingMembers().contains(recipientId);
}
}
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.GroupInviteLinkUrl;
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.HashSet;
import java.util.Set;
-import java.util.UUID;
public class GroupInfoV1 extends GroupInfo {
- private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id;
- @JsonProperty
public String name;
- @JsonProperty
- @JsonDeserialize(using = MembersDeserializer.class)
- @JsonSerialize(using = MembersSerializer.class)
- public Set<SignalServiceAddress> members = new HashSet<>();
- @JsonProperty
+ public Set<RecipientId> members = new HashSet<>();
public String color;
- @JsonProperty(defaultValue = "0")
public int messageExpirationTime;
- @JsonProperty(defaultValue = "false")
public boolean blocked;
- @JsonProperty
- public Integer inboxPosition;
- @JsonProperty(defaultValue = "false")
public boolean archived;
public GroupInfoV1(GroupIdV1 groupId) {
}
public GroupInfoV1(
- @JsonProperty("groupId") byte[] groupId,
- @JsonProperty("expectedV2Id") byte[] expectedV2Id,
- @JsonProperty("name") String name,
- @JsonProperty("members") Collection<SignalServiceAddress> members,
- @JsonProperty("avatarId") long _ignored_avatarId,
- @JsonProperty("color") String color,
- @JsonProperty("blocked") boolean blocked,
- @JsonProperty("inboxPosition") Integer inboxPosition,
- @JsonProperty("archived") boolean archived,
- @JsonProperty("messageExpirationTime") int messageExpirationTime,
- @JsonProperty("active") boolean _ignored_active
+ final GroupIdV1 groupId,
+ final GroupIdV2 expectedV2Id,
+ final String name,
+ final Set<RecipientId> members,
+ final String color,
+ final int messageExpirationTime,
+ final boolean blocked,
+ final boolean archived
) {
- this.groupId = GroupId.v1(groupId);
- this.expectedV2Id = GroupId.v2(expectedV2Id);
+ this.groupId = groupId;
+ this.expectedV2Id = expectedV2Id;
this.name = name;
- this.members.addAll(members);
+ this.members = members;
this.color = color;
+ this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
- this.inboxPosition = inboxPosition;
this.archived = archived;
- this.messageExpirationTime = messageExpirationTime;
}
@Override
- @JsonIgnore
public GroupIdV1 getGroupId() {
return groupId;
}
- @JsonProperty("groupId")
- private byte[] getGroupIdJackson() {
- return groupId.serialize();
- }
-
- @JsonIgnore
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
return expectedV2Id;
}
- @JsonProperty("expectedV2Id")
- private byte[] getExpectedV2IdJackson() {
- return getExpectedV2Id().serialize();
- }
-
@Override
public String getTitle() {
return name;
return null;
}
- @JsonIgnore
- public Set<SignalServiceAddress> getMembers() {
+ public Set<RecipientId> getMembers() {
return members;
}
return messageExpirationTime;
}
- public void addMembers(Collection<SignalServiceAddress> addresses) {
- for (var address : addresses) {
- if (this.members.contains(address)) {
- continue;
- }
- removeMember(address);
- this.members.add(address);
- }
- }
-
- public void removeMember(SignalServiceAddress address) {
- this.members.removeIf(member -> member.matches(address));
- }
-
- 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();
- }
+ public void addMembers(Collection<RecipientId> members) {
+ this.members.addAll(members);
}
- 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;
- }
+ public void removeMember(RecipientId recipientId) {
+ this.members.removeIf(member -> member.equals(recipientId));
}
}
import org.asamk.signal.manager.groups.GroupIdV2;
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.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
+ private RecipientResolver recipientResolver;
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
+ public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
+ this.groupId = groupId;
+ this.masterKey = masterKey;
+ this.blocked = blocked;
+ }
+
@Override
public GroupIdV2 getGroupId() {
return groupId;
return masterKey;
}
- public void setGroup(final DecryptedGroup group) {
+ public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
this.group = group;
+ this.recipientResolver = recipientResolver;
}
public DecryptedGroup getGroup() {
}
@Override
- public Set<SignalServiceAddress> getMembers() {
+ public Set<RecipientId> getMembers() {
if (this.group == null) {
return Set.of();
}
return group.getMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}
@Override
- public Set<SignalServiceAddress> getPendingMembers() {
+ public Set<RecipientId> getPendingMembers() {
if (this.group == null) {
return Set.of();
}
return group.getPendingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}
@Override
- public Set<SignalServiceAddress> getRequestingMembers() {
+ public Set<RecipientId> getRequestingMembers() {
if (this.group == null) {
return Set.of();
}
return group.getRequestingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet());
}
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-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;
- }
- }
-}
final var results = m.joinGroup(linkUrl);
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());
} else {
writer.println("Joined group \"{}\"", newGroupId.toBase64());
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.groups.GroupInfo;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
"Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}",
group.getGroupId().toBase64(),
group.getTitle(),
- group.isMember(m.getSelfAddress()),
+ group.isMember(m.getSelfRecipientId()),
group.isBlocked(),
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getGroupId().toBase64(),
group.getTitle(),
- group.isMember(m.getSelfAddress()),
+ group.isMember(m.getSelfRecipientId()),
group.isBlocked());
}
}
jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(),
group.getTitle(),
- group.isMember(m.getSelfAddress()),
+ group.isMember(m.getSelfRecipientId()),
group.isBlocked(),
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
if (group == null) {
return false;
} else {
- return group.isMember(m.getSelfAddress());
+ return group.isMember(m.getSelfRecipientId());
}
}
}