mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Refactor contact and profile store
This commit is contained in:
parent
a96bd91770
commit
224d8194cc
31 changed files with 1393 additions and 729 deletions
|
@ -1,6 +1,7 @@
|
||||||
package org.asamk.signal.manager;
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -160,15 +161,15 @@ class SendGroupInfoAction implements HandleAction {
|
||||||
|
|
||||||
class RetrieveProfileAction implements HandleAction {
|
class RetrieveProfileAction implements HandleAction {
|
||||||
|
|
||||||
private final SignalServiceAddress address;
|
private final RecipientId recipientId;
|
||||||
|
|
||||||
public RetrieveProfileAction(final SignalServiceAddress address) {
|
public RetrieveProfileAction(final RecipientId recipientId) {
|
||||||
this.address = address;
|
this.recipientId = recipientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(Manager m) throws Throwable {
|
public void execute(Manager m) throws Throwable {
|
||||||
m.getRecipientProfile(address, true);
|
m.getRecipientProfile(recipientId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -178,11 +179,11 @@ class RetrieveProfileAction implements HandleAction {
|
||||||
|
|
||||||
final RetrieveProfileAction that = (RetrieveProfileAction) o;
|
final RetrieveProfileAction that = (RetrieveProfileAction) o;
|
||||||
|
|
||||||
return address.equals(that.address);
|
return recipientId.equals(that.recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return address.hashCode();
|
return recipientId.hashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ import org.asamk.signal.manager.helper.PinHelper;
|
||||||
import org.asamk.signal.manager.helper.ProfileHelper;
|
import org.asamk.signal.manager.helper.ProfileHelper;
|
||||||
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
|
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.contacts.ContactInfo;
|
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
||||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
import org.asamk.signal.manager.storage.recipients.Contact;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.stickers.Sticker;
|
import org.asamk.signal.manager.storage.stickers.Sticker;
|
||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
|
@ -243,13 +243,15 @@ public class Manager implements Closeable {
|
||||||
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
|
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
|
||||||
unidentifiedAccessHelper::getAccessFor,
|
unidentifiedAccessHelper::getAccessFor,
|
||||||
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
|
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
|
||||||
() -> messageReceiver);
|
() -> messageReceiver,
|
||||||
|
this::resolveSignalServiceAddress);
|
||||||
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
|
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
|
||||||
this::getRecipientProfile,
|
this::getRecipientProfile,
|
||||||
account::getSelfAddress,
|
account::getSelfRecipientId,
|
||||||
groupsV2Operations,
|
groupsV2Operations,
|
||||||
groupsV2Api,
|
groupsV2Api,
|
||||||
this::getGroupAuthForToday);
|
this::getGroupAuthForToday,
|
||||||
|
this::resolveSignalServiceAddress);
|
||||||
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
|
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
|
||||||
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
|
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
|
||||||
}
|
}
|
||||||
|
@ -355,24 +357,26 @@ public class Manager implements Closeable {
|
||||||
* if it's Optional.absent(), the avatar will be removed
|
* if it's Optional.absent(), the avatar will be removed
|
||||||
*/
|
*/
|
||||||
public void setProfile(String name, String about, String aboutEmoji, Optional<File> avatar) throws IOException {
|
public void setProfile(String name, String about, String aboutEmoji, Optional<File> avatar) throws IOException {
|
||||||
var profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress());
|
var profile = getRecipientProfile(account.getSelfRecipientId());
|
||||||
var profile = profileEntry == null ? null : profileEntry.getProfile();
|
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
|
||||||
var newProfile = new SignalProfile(profile == null ? null : profile.getIdentityKey(),
|
if (name != null) {
|
||||||
name != null ? name : profile == null || profile.getName() == null ? "" : profile.getName(),
|
builder.withGivenName(name);
|
||||||
about != null ? about : profile == null || profile.getAbout() == null ? "" : profile.getAbout(),
|
builder.withFamilyName(null);
|
||||||
aboutEmoji != null
|
}
|
||||||
? aboutEmoji
|
if (about != null) {
|
||||||
: profile == null || profile.getAboutEmoji() == null ? "" : profile.getAboutEmoji(),
|
builder.withAbout(about);
|
||||||
profile == null ? null : profile.getUnidentifiedAccess(),
|
}
|
||||||
account.isUnrestrictedUnidentifiedAccess(),
|
if (aboutEmoji != null) {
|
||||||
profile == null ? null : profile.getCapabilities());
|
builder.withAboutEmoji(aboutEmoji);
|
||||||
|
}
|
||||||
|
var newProfile = builder.build();
|
||||||
|
|
||||||
try (final var streamDetails = avatar == null
|
try (final var streamDetails = avatar == null
|
||||||
? avatarStore.retrieveProfileAvatar(getSelfAddress())
|
? avatarStore.retrieveProfileAvatar(getSelfAddress())
|
||||||
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
|
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
|
||||||
accountManager.setVersionedProfile(account.getUuid(),
|
accountManager.setVersionedProfile(account.getUuid(),
|
||||||
account.getProfileKey(),
|
account.getProfileKey(),
|
||||||
newProfile.getName(),
|
newProfile.getInternalServiceName(),
|
||||||
newProfile.getAbout(),
|
newProfile.getAbout(),
|
||||||
newProfile.getAboutEmoji(),
|
newProfile.getAboutEmoji(),
|
||||||
streamDetails);
|
streamDetails);
|
||||||
|
@ -386,12 +390,7 @@ public class Manager implements Closeable {
|
||||||
avatarStore.deleteProfileAvatar(getSelfAddress());
|
avatarStore.deleteProfileAvatar(getSelfAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
account.getProfileStore()
|
account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
|
||||||
.updateProfile(getSelfAddress(),
|
|
||||||
account.getProfileKey(),
|
|
||||||
System.currentTimeMillis(),
|
|
||||||
newProfile,
|
|
||||||
profileEntry == null ? null : profileEntry.getProfileKeyCredential());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
||||||
|
@ -527,99 +526,124 @@ public class Manager implements Closeable {
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalProfile getRecipientProfile(
|
public Profile getRecipientProfile(
|
||||||
SignalServiceAddress address
|
SignalServiceAddress address
|
||||||
) {
|
) {
|
||||||
return getRecipientProfile(address, false);
|
return getRecipientProfile(resolveRecipient(address), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalProfile getRecipientProfile(
|
public Profile getRecipientProfile(
|
||||||
SignalServiceAddress address, boolean force
|
RecipientId recipientId
|
||||||
) {
|
) {
|
||||||
var profileEntry = account.getProfileStore().getProfileEntry(address);
|
return getRecipientProfile(recipientId, false);
|
||||||
if (profileEntry == null) {
|
}
|
||||||
// retrieve profile to get identity key
|
|
||||||
retrieveEncryptedProfile(address);
|
private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
|
||||||
|
|
||||||
|
Profile getRecipientProfile(
|
||||||
|
RecipientId recipientId, boolean force
|
||||||
|
) {
|
||||||
|
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||||
|
if (profileKey == null) {
|
||||||
|
if (force) {
|
||||||
|
// retrieve profile to get identity key
|
||||||
|
retrieveEncryptedProfile(recipientId);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var now = new Date().getTime();
|
var profile = account.getProfileStore().getProfile(recipientId);
|
||||||
// Profiles are cached for 24h before retrieving them again
|
|
||||||
if (!profileEntry.isRequestPending() && (
|
|
||||||
force
|
|
||||||
|| profileEntry.getProfile() == null
|
|
||||||
|| now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
|
|
||||||
)) {
|
|
||||||
profileEntry.setRequestPending(true);
|
|
||||||
final SignalServiceProfile encryptedProfile;
|
|
||||||
try {
|
|
||||||
encryptedProfile = retrieveEncryptedProfile(address);
|
|
||||||
} finally {
|
|
||||||
profileEntry.setRequestPending(false);
|
|
||||||
}
|
|
||||||
if (encryptedProfile == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final var profileKey = profileEntry.getProfileKey();
|
var now = new Date().getTime();
|
||||||
final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile);
|
// Profiles are cached for 24h before retrieving them again, unless forced
|
||||||
account.getProfileStore()
|
if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
|
||||||
.updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
return profileEntry.getProfile();
|
|
||||||
|
synchronized (pendingProfileRequest) {
|
||||||
|
if (pendingProfileRequest.contains(recipientId)) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
pendingProfileRequest.add(recipientId);
|
||||||
|
}
|
||||||
|
final SignalServiceProfile encryptedProfile;
|
||||||
|
try {
|
||||||
|
encryptedProfile = retrieveEncryptedProfile(recipientId);
|
||||||
|
} finally {
|
||||||
|
synchronized (pendingProfileRequest) {
|
||||||
|
pendingProfileRequest.remove(recipientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (encryptedProfile == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
|
||||||
|
account.getProfileStore().storeProfile(recipientId, profile);
|
||||||
|
|
||||||
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceProfile retrieveEncryptedProfile(SignalServiceAddress address) {
|
private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
|
||||||
try {
|
try {
|
||||||
final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE)
|
return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
|
||||||
.getProfile();
|
|
||||||
try {
|
|
||||||
account.getIdentityKeyStore()
|
|
||||||
.saveIdentity(resolveRecipient(address),
|
|
||||||
new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
|
|
||||||
new Date());
|
|
||||||
} catch (InvalidKeyException ignored) {
|
|
||||||
logger.warn("Got invalid identity key in profile for {}", address.getLegacyIdentifier());
|
|
||||||
}
|
|
||||||
return profile;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) {
|
private ProfileAndCredential retrieveProfileAndCredential(
|
||||||
var profileEntry = account.getProfileStore().getProfileEntry(address);
|
final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
|
||||||
if (profileEntry == null) {
|
) throws IOException {
|
||||||
return null;
|
final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
|
||||||
}
|
final var profile = profileAndCredential.getProfile();
|
||||||
if (profileEntry.getProfileKeyCredential() == null) {
|
|
||||||
ProfileAndCredential profileAndCredential;
|
|
||||||
try {
|
|
||||||
profileAndCredential = profileHelper.retrieveProfileSync(address,
|
|
||||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = new Date().getTime();
|
try {
|
||||||
final var profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
|
account.getIdentityKeyStore()
|
||||||
final var profile = decryptProfileAndDownloadAvatar(address,
|
.saveIdentity(recipientId,
|
||||||
profileEntry.getProfileKey(),
|
new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
|
||||||
profileAndCredential.getProfile());
|
new Date());
|
||||||
account.getProfileStore()
|
} catch (InvalidKeyException ignored) {
|
||||||
.updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential);
|
logger.warn("Got invalid identity key in profile for {}",
|
||||||
return profileKeyCredential;
|
resolveSignalServiceAddress(recipientId).getLegacyIdentifier());
|
||||||
}
|
}
|
||||||
return profileEntry.getProfileKeyCredential();
|
return profileAndCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalProfile decryptProfileAndDownloadAvatar(
|
private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
|
||||||
final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
|
||||||
|
if (profileKeyCredential != null) {
|
||||||
|
return profileKeyCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileAndCredential profileAndCredential;
|
||||||
|
try {
|
||||||
|
profileAndCredential = retrieveProfileAndCredential(recipientId,
|
||||||
|
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
|
||||||
|
account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
|
||||||
|
|
||||||
|
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||||
|
if (profileKey != null) {
|
||||||
|
final var profile = decryptProfileAndDownloadAvatar(recipientId,
|
||||||
|
profileKey,
|
||||||
|
profileAndCredential.getProfile());
|
||||||
|
account.getProfileStore().storeProfile(recipientId, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileKeyCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Profile decryptProfileAndDownloadAvatar(
|
||||||
|
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
||||||
) {
|
) {
|
||||||
if (encryptedProfile.getAvatar() != null) {
|
if (encryptedProfile.getAvatar() != null) {
|
||||||
downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
|
downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
||||||
|
@ -729,19 +753,23 @@ public class Manager implements Closeable {
|
||||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
|
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
|
||||||
return sendUpdateGroupMessage(groupId,
|
return sendUpdateGroupMessage(groupId,
|
||||||
name,
|
name,
|
||||||
members == null ? null : getSignalServiceAddresses(members),
|
members == null
|
||||||
|
? null
|
||||||
|
: getSignalServiceAddresses(members).stream()
|
||||||
|
.map(this::resolveRecipient)
|
||||||
|
.collect(Collectors.toSet()),
|
||||||
avatarFile);
|
avatarFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
|
private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
|
||||||
GroupId groupId, String name, Collection<SignalServiceAddress> members, File avatarFile
|
GroupId groupId, String name, Set<RecipientId> members, File avatarFile
|
||||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
|
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
|
||||||
GroupInfo g;
|
GroupInfo g;
|
||||||
SignalServiceDataMessage.Builder messageBuilder;
|
SignalServiceDataMessage.Builder messageBuilder;
|
||||||
if (groupId == null) {
|
if (groupId == null) {
|
||||||
// Create new group
|
// Create new group
|
||||||
var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
|
var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
|
||||||
members == null ? List.of() : members,
|
members == null ? Set.of() : members,
|
||||||
avatarFile);
|
avatarFile);
|
||||||
if (gv2 == null) {
|
if (gv2 == null) {
|
||||||
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
|
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
|
||||||
|
@ -774,7 +802,7 @@ public class Manager implements Closeable {
|
||||||
final var newMembers = new HashSet<>(members);
|
final var newMembers = new HashSet<>(members);
|
||||||
newMembers.removeAll(group.getMembers()
|
newMembers.removeAll(group.getMembers()
|
||||||
.stream()
|
.stream()
|
||||||
.map(this::resolveSignalServiceAddress)
|
.map(this::resolveRecipient)
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
if (newMembers.size() > 0) {
|
if (newMembers.size() > 0) {
|
||||||
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
|
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
|
||||||
|
@ -810,18 +838,18 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateGroupV1(
|
private void updateGroupV1(
|
||||||
final GroupInfoV1 g,
|
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
|
||||||
final String name,
|
|
||||||
final Collection<SignalServiceAddress> members,
|
|
||||||
final File avatarFile
|
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
g.name = name;
|
g.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members != null) {
|
if (members != null) {
|
||||||
|
final var memberAddresses = members.stream()
|
||||||
|
.map(this::resolveSignalServiceAddress)
|
||||||
|
.collect(Collectors.toList());
|
||||||
final var newE164Members = new HashSet<String>();
|
final var newE164Members = new HashSet<String>();
|
||||||
for (var member : members) {
|
for (var member : memberAddresses) {
|
||||||
if (g.isMember(member) || !member.getNumber().isPresent()) {
|
if (g.isMember(member) || !member.getNumber().isPresent()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -837,7 +865,7 @@ public class Manager implements Closeable {
|
||||||
+ " to group: Not registered on Signal");
|
+ " to group: Not registered on Signal");
|
||||||
}
|
}
|
||||||
|
|
||||||
g.addMembers(members);
|
g.addMembers(memberAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarFile != null) {
|
if (avatarFile != null) {
|
||||||
|
@ -973,7 +1001,7 @@ public class Manager implements Closeable {
|
||||||
System.currentTimeMillis());
|
System.currentTimeMillis());
|
||||||
|
|
||||||
createMessageSender().sendReceipt(remoteAddress,
|
createMessageSender().sendReceipt(remoteAddress,
|
||||||
unidentifiedAccessHelper.getAccessFor(remoteAddress),
|
unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)),
|
||||||
receiptMessage);
|
receiptMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1053,36 +1081,26 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getContactName(String number) throws InvalidNumberException {
|
public String getContactName(String number) throws InvalidNumberException {
|
||||||
var contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number));
|
var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number));
|
||||||
if (contact == null) {
|
return contact == null || contact.getName() == null ? "" : contact.getName();
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return contact.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactName(String number, String name) throws InvalidNumberException {
|
public void setContactName(String number, String name) throws InvalidNumberException {
|
||||||
final var address = canonicalizeAndResolveSignalServiceAddress(number);
|
final var recipientId = canonicalizeAndResolveRecipient(number);
|
||||||
var contact = account.getContactStore().getContact(address);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact == null) {
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
contact = new ContactInfo(address);
|
account.getContactStore().storeContact(recipientId, builder.withName(name).build());
|
||||||
}
|
|
||||||
contact.name = name;
|
|
||||||
account.getContactStore().updateContact(contact);
|
|
||||||
account.save();
|
account.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
|
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
|
||||||
setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked);
|
setContactBlocked(canonicalizeAndResolveRecipient(number), blocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setContactBlocked(SignalServiceAddress address, boolean blocked) {
|
private void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
||||||
var contact = account.getContactStore().getContact(address);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact == null) {
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
contact = new ContactInfo(address);
|
account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build());
|
||||||
}
|
|
||||||
contact.blocked = blocked;
|
|
||||||
account.getContactStore().updateContact(contact);
|
|
||||||
account.save();
|
account.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1097,15 +1115,14 @@ public class Manager implements Closeable {
|
||||||
account.save();
|
account.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
|
||||||
* Change the expiration timer for a contact
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
*/
|
if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
|
||||||
public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException {
|
return;
|
||||||
var contact = account.getContactStore().getContact(address);
|
}
|
||||||
contact.messageExpirationTime = messageExpirationTimer;
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
account.getContactStore().updateContact(contact);
|
account.getContactStore()
|
||||||
sendExpirationTimerUpdate(address);
|
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
|
||||||
account.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
|
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
|
||||||
|
@ -1119,8 +1136,10 @@ public class Manager implements Closeable {
|
||||||
public void setExpirationTimer(
|
public void setExpirationTimer(
|
||||||
String number, int messageExpirationTimer
|
String number, int messageExpirationTimer
|
||||||
) throws IOException, InvalidNumberException {
|
) throws IOException, InvalidNumberException {
|
||||||
var address = canonicalizeAndResolveSignalServiceAddress(number);
|
var recipientId = canonicalizeAndResolveRecipient(number);
|
||||||
setExpirationTimer(address, messageExpirationTimer);
|
setExpirationTimer(recipientId, messageExpirationTimer);
|
||||||
|
sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
|
||||||
|
account.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1298,6 +1317,7 @@ public class Manager implements Closeable {
|
||||||
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
|
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
|
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
|
||||||
|
final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet());
|
||||||
final var timestamp = System.currentTimeMillis();
|
final var timestamp = System.currentTimeMillis();
|
||||||
messageBuilder.withTimestamp(timestamp);
|
messageBuilder.withTimestamp(timestamp);
|
||||||
getOrCreateMessagePipe();
|
getOrCreateMessagePipe();
|
||||||
|
@ -1310,7 +1330,7 @@ public class Manager implements Closeable {
|
||||||
var messageSender = createMessageSender();
|
var messageSender = createMessageSender();
|
||||||
final var isRecipientUpdate = false;
|
final var isRecipientUpdate = false;
|
||||||
var result = messageSender.sendMessage(new ArrayList<>(recipients),
|
var result = messageSender.sendMessage(new ArrayList<>(recipients),
|
||||||
unidentifiedAccessHelper.getAccessFor(recipients),
|
unidentifiedAccessHelper.getAccessFor(recipientIds),
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
message);
|
message);
|
||||||
|
|
||||||
|
@ -1332,8 +1352,8 @@ public class Manager implements Closeable {
|
||||||
messageBuilder.withProfileKey(account.getProfileKey().serialize());
|
messageBuilder.withProfileKey(account.getProfileKey().serialize());
|
||||||
var results = new ArrayList<SendMessageResult>(recipients.size());
|
var results = new ArrayList<SendMessageResult>(recipients.size());
|
||||||
for (var address : recipients) {
|
for (var address : recipients) {
|
||||||
final var contact = account.getContactStore().getContact(address);
|
final var contact = account.getContactStore().getContact(resolveRecipient(address));
|
||||||
final var expirationTime = contact != null ? contact.messageExpirationTime : 0;
|
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
|
||||||
messageBuilder.withExpiration(expirationTime);
|
messageBuilder.withExpiration(expirationTime);
|
||||||
message = messageBuilder.build();
|
message = messageBuilder.build();
|
||||||
results.add(sendMessage(address, message));
|
results.add(sendMessage(address, message));
|
||||||
|
@ -1358,10 +1378,10 @@ public class Manager implements Closeable {
|
||||||
getOrCreateMessagePipe();
|
getOrCreateMessagePipe();
|
||||||
getOrCreateUnidentifiedMessagePipe();
|
getOrCreateUnidentifiedMessagePipe();
|
||||||
try {
|
try {
|
||||||
final var address = getSelfAddress();
|
final var recipientId = account.getSelfRecipientId();
|
||||||
|
|
||||||
final var contact = account.getContactStore().getContact(address);
|
final var contact = account.getContactStore().getContact(recipientId);
|
||||||
final var expirationTime = contact != null ? contact.messageExpirationTime : 0;
|
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
|
||||||
messageBuilder.withExpiration(expirationTime);
|
messageBuilder.withExpiration(expirationTime);
|
||||||
|
|
||||||
var message = messageBuilder.build();
|
var message = messageBuilder.build();
|
||||||
|
@ -1377,7 +1397,7 @@ public class Manager implements Closeable {
|
||||||
|
|
||||||
var recipient = account.getSelfAddress();
|
var recipient = account.getSelfAddress();
|
||||||
|
|
||||||
final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient);
|
final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(resolveRecipient(recipient));
|
||||||
var transcript = new SentTranscriptMessage(Optional.of(recipient),
|
var transcript = new SentTranscriptMessage(Optional.of(recipient),
|
||||||
message.getTimestamp(),
|
message.getTimestamp(),
|
||||||
message,
|
message,
|
||||||
|
@ -1404,7 +1424,9 @@ public class Manager implements Closeable {
|
||||||
var messageSender = createMessageSender();
|
var messageSender = createMessageSender();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
|
return messageSender.sendMessage(address,
|
||||||
|
unidentifiedAccessHelper.getAccessFor(resolveRecipient(address)),
|
||||||
|
message);
|
||||||
} catch (UntrustedIdentityException e) {
|
} catch (UntrustedIdentityException e) {
|
||||||
return SendMessageResult.identityFailure(address, e.getIdentityKey());
|
return SendMessageResult.identityFailure(address, e.getIdentityKey());
|
||||||
}
|
}
|
||||||
|
@ -1520,14 +1542,7 @@ public class Manager implements Closeable {
|
||||||
// disappearing message timer already stored in the DecryptedGroup
|
// disappearing message timer already stored in the DecryptedGroup
|
||||||
}
|
}
|
||||||
} else if (conversationPartnerAddress != null) {
|
} else if (conversationPartnerAddress != null) {
|
||||||
var contact = account.getContactStore().getContact(conversationPartnerAddress);
|
setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds());
|
||||||
if (contact == null) {
|
|
||||||
contact = new ContactInfo(conversationPartnerAddress);
|
|
||||||
}
|
|
||||||
if (contact.messageExpirationTime != message.getExpiresInSeconds()) {
|
|
||||||
contact.messageExpirationTime = message.getExpiresInSeconds();
|
|
||||||
account.getContactStore().updateContact(contact);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ignoreAttachments) {
|
if (!ignoreAttachments) {
|
||||||
|
@ -1554,7 +1569,7 @@ public class Manager implements Closeable {
|
||||||
if (source.matches(account.getSelfAddress())) {
|
if (source.matches(account.getSelfAddress())) {
|
||||||
this.account.setProfileKey(profileKey);
|
this.account.setProfileKey(profileKey);
|
||||||
}
|
}
|
||||||
this.account.getProfileStore().storeProfileKey(source, profileKey);
|
this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey);
|
||||||
}
|
}
|
||||||
if (message.getPreviews().isPresent()) {
|
if (message.getPreviews().isPresent()) {
|
||||||
final var previews = message.getPreviews().get();
|
final var previews = message.getPreviews().get();
|
||||||
|
@ -1632,7 +1647,7 @@ public class Manager implements Closeable {
|
||||||
|
|
||||||
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
||||||
for (var member : group.getMembersList()) {
|
for (var member : group.getMembersList()) {
|
||||||
final var address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid()
|
final var address = resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid()
|
||||||
.toByteArray()), null));
|
.toByteArray()), null));
|
||||||
try {
|
try {
|
||||||
account.getProfileStore()
|
account.getProfileStore()
|
||||||
|
@ -1789,7 +1804,7 @@ public class Manager implements Closeable {
|
||||||
if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
|
if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
|
||||||
final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception)
|
final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception)
|
||||||
.getName());
|
.getName());
|
||||||
queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId)));
|
queuedActions.add(new RetrieveProfileAction(recipientId));
|
||||||
if (!envelope.hasSource()) {
|
if (!envelope.hasSource()) {
|
||||||
try {
|
try {
|
||||||
cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
|
cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
|
||||||
|
@ -1816,8 +1831,8 @@ public class Manager implements Closeable {
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var sourceContact = account.getContactStore().getContact(source);
|
final var recipientId = resolveRecipient(source);
|
||||||
if (sourceContact != null && sourceContact.blocked) {
|
if (isContactBlocked(recipientId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1834,6 +1849,16 @@ public class Manager implements Closeable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isContactBlocked(final String identifier) throws InvalidNumberException {
|
||||||
|
final var recipientId = canonicalizeAndResolveRecipient(identifier);
|
||||||
|
return isContactBlocked(recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isContactBlocked(final RecipientId recipientId) {
|
||||||
|
var sourceContact = account.getContactStore().getContact(recipientId);
|
||||||
|
return sourceContact != null && sourceContact.isBlocked();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isNotAGroupMember(
|
private boolean isNotAGroupMember(
|
||||||
SignalServiceEnvelope envelope, SignalServiceContent content
|
SignalServiceEnvelope envelope, SignalServiceContent content
|
||||||
) {
|
) {
|
||||||
|
@ -1876,8 +1901,6 @@ public class Manager implements Closeable {
|
||||||
} else {
|
} else {
|
||||||
sender = content.getSender();
|
sender = content.getSender();
|
||||||
}
|
}
|
||||||
// Store uuid if we don't have it already
|
|
||||||
resolveSignalServiceAddress(sender);
|
|
||||||
|
|
||||||
if (content.getDataMessage().isPresent()) {
|
if (content.getDataMessage().isPresent()) {
|
||||||
var message = content.getDataMessage().get();
|
var message = content.getDataMessage().get();
|
||||||
|
@ -1974,7 +1997,7 @@ public class Manager implements Closeable {
|
||||||
if (syncMessage.getBlockedList().isPresent()) {
|
if (syncMessage.getBlockedList().isPresent()) {
|
||||||
final var blockedListMessage = syncMessage.getBlockedList().get();
|
final var blockedListMessage = syncMessage.getBlockedList().get();
|
||||||
for (var address : blockedListMessage.getAddresses()) {
|
for (var address : blockedListMessage.getAddresses()) {
|
||||||
setContactBlocked(resolveSignalServiceAddress(address), true);
|
setContactBlocked(resolveRecipient(address), true);
|
||||||
}
|
}
|
||||||
for (var groupId : blockedListMessage.getGroupIds()
|
for (var groupId : blockedListMessage.getGroupIds()
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -2001,19 +2024,19 @@ public class Manager implements Closeable {
|
||||||
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
|
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
|
||||||
account.setProfileKey(c.getProfileKey().get());
|
account.setProfileKey(c.getProfileKey().get());
|
||||||
}
|
}
|
||||||
final var address = resolveSignalServiceAddress(c.getAddress());
|
final var recipientId = resolveRecipientTrusted(c.getAddress());
|
||||||
var contact = account.getContactStore().getContact(address);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
if (contact == null) {
|
final var builder = contact == null
|
||||||
contact = new ContactInfo(address);
|
? Contact.newBuilder()
|
||||||
}
|
: Contact.newBuilder(contact);
|
||||||
if (c.getName().isPresent()) {
|
if (c.getName().isPresent()) {
|
||||||
contact.name = c.getName().get();
|
builder.withName(c.getName().get());
|
||||||
}
|
}
|
||||||
if (c.getColor().isPresent()) {
|
if (c.getColor().isPresent()) {
|
||||||
contact.color = c.getColor().get();
|
builder.withColor(c.getColor().get());
|
||||||
}
|
}
|
||||||
if (c.getProfileKey().isPresent()) {
|
if (c.getProfileKey().isPresent()) {
|
||||||
account.getProfileStore().storeProfileKey(address, c.getProfileKey().get());
|
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
|
||||||
}
|
}
|
||||||
if (c.getVerified().isPresent()) {
|
if (c.getVerified().isPresent()) {
|
||||||
final var verifiedMessage = c.getVerified().get();
|
final var verifiedMessage = c.getVerified().get();
|
||||||
|
@ -2023,15 +2046,14 @@ public class Manager implements Closeable {
|
||||||
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
||||||
}
|
}
|
||||||
if (c.getExpirationTimer().isPresent()) {
|
if (c.getExpirationTimer().isPresent()) {
|
||||||
contact.messageExpirationTime = c.getExpirationTimer().get();
|
builder.withMessageExpirationTime(c.getExpirationTimer().get());
|
||||||
}
|
}
|
||||||
contact.blocked = c.isBlocked();
|
builder.withBlocked(c.isBlocked());
|
||||||
contact.inboxPosition = c.getInboxPosition().orNull();
|
builder.withArchived(c.isArchived());
|
||||||
contact.archived = c.isArchived();
|
account.getContactStore().storeContact(recipientId, builder.build());
|
||||||
account.getContactStore().updateContact(contact);
|
|
||||||
|
|
||||||
if (c.getAvatar().isPresent()) {
|
if (c.getAvatar().isPresent()) {
|
||||||
downloadContactAvatar(c.getAvatar().get(), contact.getAddress());
|
downloadContactAvatar(c.getAvatar().get(), c.getAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2079,7 +2101,7 @@ public class Manager implements Closeable {
|
||||||
if (syncMessage.getFetchType().isPresent()) {
|
if (syncMessage.getFetchType().isPresent()) {
|
||||||
switch (syncMessage.getFetchType().get()) {
|
switch (syncMessage.getFetchType().get()) {
|
||||||
case LOCAL_PROFILE:
|
case LOCAL_PROFILE:
|
||||||
getRecipientProfile(getSelfAddress(), true);
|
getRecipientProfile(account.getSelfRecipientId(), true);
|
||||||
case STORAGE_MANIFEST:
|
case STORAGE_MANIFEST:
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
@ -2294,28 +2316,31 @@ public class Manager implements Closeable {
|
||||||
try {
|
try {
|
||||||
try (OutputStream fos = new FileOutputStream(contactsFile)) {
|
try (OutputStream fos = new FileOutputStream(contactsFile)) {
|
||||||
var out = new DeviceContactsOutputStream(fos);
|
var out = new DeviceContactsOutputStream(fos);
|
||||||
for (var record : account.getContactStore().getContacts()) {
|
for (var contactPair : account.getContactStore().getContacts()) {
|
||||||
|
final var recipientId = contactPair.first();
|
||||||
|
final var contact = contactPair.second();
|
||||||
|
final var address = resolveSignalServiceAddress(recipientId);
|
||||||
|
|
||||||
|
var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
|
||||||
VerifiedMessage verifiedMessage = null;
|
VerifiedMessage verifiedMessage = null;
|
||||||
var currentIdentity = account.getIdentityKeyStore()
|
|
||||||
.getIdentity(resolveRecipientTrusted(record.getAddress()));
|
|
||||||
if (currentIdentity != null) {
|
if (currentIdentity != null) {
|
||||||
verifiedMessage = new VerifiedMessage(record.getAddress(),
|
verifiedMessage = new VerifiedMessage(address,
|
||||||
currentIdentity.getIdentityKey(),
|
currentIdentity.getIdentityKey(),
|
||||||
currentIdentity.getTrustLevel().toVerifiedState(),
|
currentIdentity.getTrustLevel().toVerifiedState(),
|
||||||
currentIdentity.getDateAdded().getTime());
|
currentIdentity.getDateAdded().getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileKey = account.getProfileStore().getProfileKey(record.getAddress());
|
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||||
out.write(new DeviceContact(record.getAddress(),
|
out.write(new DeviceContact(address,
|
||||||
Optional.fromNullable(record.name),
|
Optional.fromNullable(contact.getName()),
|
||||||
createContactAvatarAttachment(record.getAddress()),
|
createContactAvatarAttachment(address),
|
||||||
Optional.fromNullable(record.color),
|
Optional.fromNullable(contact.getColor()),
|
||||||
Optional.fromNullable(verifiedMessage),
|
Optional.fromNullable(verifiedMessage),
|
||||||
Optional.fromNullable(profileKey),
|
Optional.fromNullable(profileKey),
|
||||||
record.blocked,
|
contact.isBlocked(),
|
||||||
Optional.of(record.messageExpirationTime),
|
Optional.of(contact.getMessageExpirationTime()),
|
||||||
Optional.fromNullable(record.inboxPosition),
|
Optional.absent(),
|
||||||
record.archived));
|
contact.isArchived()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.getProfileKey() != null) {
|
if (account.getProfileKey() != null) {
|
||||||
|
@ -2356,8 +2381,8 @@ public class Manager implements Closeable {
|
||||||
void sendBlockedList() throws IOException, UntrustedIdentityException {
|
void sendBlockedList() throws IOException, UntrustedIdentityException {
|
||||||
var addresses = new ArrayList<SignalServiceAddress>();
|
var addresses = new ArrayList<SignalServiceAddress>();
|
||||||
for (var record : account.getContactStore().getContacts()) {
|
for (var record : account.getContactStore().getContacts()) {
|
||||||
if (record.blocked) {
|
if (record.second().isBlocked()) {
|
||||||
addresses.add(record.getAddress());
|
addresses.add(resolveSignalServiceAddress(record.first()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var groupIds = new ArrayList<byte[]>();
|
var groupIds = new ArrayList<byte[]>();
|
||||||
|
@ -2379,22 +2404,25 @@ public class Manager implements Closeable {
|
||||||
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
|
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ContactInfo> getContacts() {
|
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||||
return account.getContactStore().getContacts();
|
return account.getContactStore().getContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getContactOrProfileName(String number) {
|
public String getContactOrProfileName(String number) throws InvalidNumberException {
|
||||||
final var address = Utils.getSignalServiceAddressFromIdentifier(number);
|
final var recipientId = canonicalizeAndResolveRecipient(number);
|
||||||
|
final var recipient = account.getRecipientStore().getRecipient(recipientId);
|
||||||
final var contact = account.getContactStore().getContact(address);
|
if (recipient == null) {
|
||||||
if (contact != null && !Util.isEmpty(contact.name)) {
|
return null;
|
||||||
return contact.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var profileEntry = account.getProfileStore().getProfileEntry(address);
|
if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
|
||||||
if (profileEntry != null && profileEntry.getProfile() != null) {
|
return recipient.getContact().getName();
|
||||||
return profileEntry.getProfile().getDisplayName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipient.getProfile() != null && recipient.getProfile() != null) {
|
||||||
|
return recipient.getProfile().getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2530,11 +2558,11 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientId resolveRecipient(SignalServiceAddress address) {
|
public RecipientId resolveRecipient(SignalServiceAddress address) {
|
||||||
return account.getRecipientStore().resolveRecipientUntrusted(address);
|
return account.getRecipientStore().resolveRecipient(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
|
private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
|
||||||
return account.getRecipientStore().resolveRecipient(address);
|
return account.getRecipientStore().resolveRecipientTrusted(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -165,7 +165,7 @@ public class RegistrationManager implements Closeable {
|
||||||
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
|
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
account.getSessionStore().archiveAllSessions();
|
account.getSessionStore().archiveAllSessions();
|
||||||
final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress());
|
final var recipientId = account.getRecipientStore().resolveRecipientTrusted(account.getSelfAddress());
|
||||||
final var publicKey = account.getIdentityKeyPair().getPublicKey();
|
final var publicKey = account.getIdentityKeyPair().getPublicKey();
|
||||||
account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
|
account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
|
||||||
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
|
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
|
||||||
|
|
|
@ -5,7 +5,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.signal.storageservice.protos.groups.AccessControl;
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
import org.signal.storageservice.protos.groups.GroupChange;
|
||||||
|
@ -38,7 +39,6 @@ import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -51,7 +51,7 @@ public class GroupHelper {
|
||||||
|
|
||||||
private final ProfileProvider profileProvider;
|
private final ProfileProvider profileProvider;
|
||||||
|
|
||||||
private final SelfAddressProvider selfAddressProvider;
|
private final SelfRecipientIdProvider selfRecipientIdProvider;
|
||||||
|
|
||||||
private final GroupsV2Operations groupsV2Operations;
|
private final GroupsV2Operations groupsV2Operations;
|
||||||
|
|
||||||
|
@ -59,20 +59,24 @@ public class GroupHelper {
|
||||||
|
|
||||||
private final GroupAuthorizationProvider groupAuthorizationProvider;
|
private final GroupAuthorizationProvider groupAuthorizationProvider;
|
||||||
|
|
||||||
|
private final SignalServiceAddressResolver addressResolver;
|
||||||
|
|
||||||
public GroupHelper(
|
public GroupHelper(
|
||||||
final ProfileKeyCredentialProvider profileKeyCredentialProvider,
|
final ProfileKeyCredentialProvider profileKeyCredentialProvider,
|
||||||
final ProfileProvider profileProvider,
|
final ProfileProvider profileProvider,
|
||||||
final SelfAddressProvider selfAddressProvider,
|
final SelfRecipientIdProvider selfRecipientIdProvider,
|
||||||
final GroupsV2Operations groupsV2Operations,
|
final GroupsV2Operations groupsV2Operations,
|
||||||
final GroupsV2Api groupsV2Api,
|
final GroupsV2Api groupsV2Api,
|
||||||
final GroupAuthorizationProvider groupAuthorizationProvider
|
final GroupAuthorizationProvider groupAuthorizationProvider,
|
||||||
|
final SignalServiceAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
|
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
|
||||||
this.profileProvider = profileProvider;
|
this.profileProvider = profileProvider;
|
||||||
this.selfAddressProvider = selfAddressProvider;
|
this.selfRecipientIdProvider = selfRecipientIdProvider;
|
||||||
this.groupsV2Operations = groupsV2Operations;
|
this.groupsV2Operations = groupsV2Operations;
|
||||||
this.groupsV2Api = groupsV2Api;
|
this.groupsV2Api = groupsV2Api;
|
||||||
this.groupAuthorizationProvider = groupAuthorizationProvider;
|
this.groupAuthorizationProvider = groupAuthorizationProvider;
|
||||||
|
this.addressResolver = addressResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
|
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
|
||||||
|
@ -97,7 +101,7 @@ public class GroupHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupInfoV2 createGroupV2(
|
public GroupInfoV2 createGroupV2(
|
||||||
String name, Collection<SignalServiceAddress> members, File avatarFile
|
String name, Set<RecipientId> members, File avatarFile
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var avatarBytes = readAvatarBytes(avatarFile);
|
final var avatarBytes = readAvatarBytes(avatarFile);
|
||||||
final var newGroup = buildNewGroupV2(name, members, avatarBytes);
|
final var newGroup = buildNewGroupV2(name, members, avatarBytes);
|
||||||
|
@ -139,9 +143,9 @@ public class GroupHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupsV2Operations.NewGroup buildNewGroupV2(
|
private GroupsV2Operations.NewGroup buildNewGroupV2(
|
||||||
String name, Collection<SignalServiceAddress> members, byte[] avatar
|
String name, Set<RecipientId> members, byte[] avatar
|
||||||
) {
|
) {
|
||||||
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddressProvider.getSelfAddress());
|
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId());
|
||||||
if (profileKeyCredential == null) {
|
if (profileKeyCredential == null) {
|
||||||
logger.warn("Cannot create a V2 group as self does not have a versioned profile");
|
logger.warn("Cannot create a V2 group as self does not have a versioned profile");
|
||||||
return null;
|
return null;
|
||||||
|
@ -149,10 +153,11 @@ public class GroupHelper {
|
||||||
|
|
||||||
if (!areMembersValid(members)) return null;
|
if (!areMembersValid(members)) return null;
|
||||||
|
|
||||||
var self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
|
var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
|
||||||
Optional.fromNullable(profileKeyCredential));
|
.getUuid()
|
||||||
|
.orNull(), Optional.fromNullable(profileKeyCredential));
|
||||||
var candidates = members.stream()
|
var candidates = members.stream()
|
||||||
.map(member -> new GroupCandidate(member.getUuid().get(),
|
.map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
|
||||||
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
|
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
@ -166,8 +171,9 @@ public class GroupHelper {
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
|
private boolean areMembersValid(final Set<RecipientId> members) {
|
||||||
final var noUuidCapability = members.stream()
|
final var noUuidCapability = members.stream()
|
||||||
|
.map(addressResolver::resolveSignalServiceAddress)
|
||||||
.filter(address -> !address.getUuid().isPresent())
|
.filter(address -> !address.getUuid().isPresent())
|
||||||
.map(SignalServiceAddress::getLegacyIdentifier)
|
.map(SignalServiceAddress::getLegacyIdentifier)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
@ -179,11 +185,11 @@ public class GroupHelper {
|
||||||
|
|
||||||
final var noGv2Capability = members.stream()
|
final var noGv2Capability = members.stream()
|
||||||
.map(profileProvider::getProfile)
|
.map(profileProvider::getProfile)
|
||||||
.filter(profile -> profile != null && !profile.getCapabilities().gv2)
|
.filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
if (noGv2Capability.size() > 0) {
|
if (noGv2Capability.size() > 0) {
|
||||||
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
|
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
|
||||||
noGv2Capability.stream().map(SignalProfile::getDisplayName).collect(Collectors.joining(", ")));
|
noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +212,8 @@ public class GroupHelper {
|
||||||
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
|
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
final var uuid = this.selfAddressProvider.getSelfAddress().getUuid();
|
final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId())
|
||||||
|
.getUuid();
|
||||||
if (uuid.isPresent()) {
|
if (uuid.isPresent()) {
|
||||||
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
|
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
|
||||||
}
|
}
|
||||||
|
@ -215,7 +222,7 @@ public class GroupHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair<DecryptedGroup, GroupChange> updateGroupV2(
|
public Pair<DecryptedGroup, GroupChange> updateGroupV2(
|
||||||
GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
|
GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||||
var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||||
|
@ -225,24 +232,25 @@ public class GroupHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
var candidates = newMembers.stream()
|
var candidates = newMembers.stream()
|
||||||
.map(member -> new GroupCandidate(member.getUuid().get(),
|
.map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
|
||||||
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
|
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
final var change = groupOperations.createModifyGroupMembershipChange(candidates,
|
final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
|
||||||
selfAddressProvider.getSelfAddress().getUuid().get());
|
.getUuid()
|
||||||
|
.get();
|
||||||
|
final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid);
|
||||||
|
|
||||||
final var uuid = this.selfAddressProvider.getSelfAddress().getUuid();
|
change.setSourceUuid(UuidUtil.toByteString(uuid));
|
||||||
if (uuid.isPresent()) {
|
|
||||||
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
|
public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
|
||||||
var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
|
var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
|
||||||
final var selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
|
final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
|
||||||
|
.getUuid()
|
||||||
|
.get();
|
||||||
var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
|
var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
|
||||||
|
|
||||||
if (selfPendingMember.isPresent()) {
|
if (selfPendingMember.isPresent()) {
|
||||||
|
@ -260,8 +268,8 @@ public class GroupHelper {
|
||||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||||
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||||
|
|
||||||
final var selfAddress = this.selfAddressProvider.getSelfAddress();
|
final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
|
||||||
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress);
|
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
|
||||||
if (profileKeyCredential == null) {
|
if (profileKeyCredential == null) {
|
||||||
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
||||||
}
|
}
|
||||||
|
@ -271,7 +279,9 @@ public class GroupHelper {
|
||||||
? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
||||||
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
||||||
|
|
||||||
change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
|
change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId)
|
||||||
|
.getUuid()
|
||||||
|
.get()));
|
||||||
|
|
||||||
return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
|
return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
|
||||||
}
|
}
|
||||||
|
@ -280,15 +290,15 @@ public class GroupHelper {
|
||||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||||
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
|
||||||
|
|
||||||
final var selfAddress = this.selfAddressProvider.getSelfAddress();
|
final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
|
||||||
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress);
|
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
|
||||||
if (profileKeyCredential == null) {
|
if (profileKeyCredential == null) {
|
||||||
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
|
final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
|
||||||
|
|
||||||
final var uuid = selfAddress.getUuid();
|
final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid();
|
||||||
if (uuid.isPresent()) {
|
if (uuid.isPresent()) {
|
||||||
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
|
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
|
||||||
}
|
}
|
||||||
|
@ -330,7 +340,9 @@ public class GroupHelper {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decryptedChange = groupOperations.decryptChange(changeActions,
|
decryptedChange = groupOperations.decryptChange(changeActions,
|
||||||
selfAddressProvider.getSelfAddress().getUuid().get());
|
addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
|
||||||
|
.getUuid()
|
||||||
|
.get());
|
||||||
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
|
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
|
||||||
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
|
@ -27,23 +28,27 @@ public final class ProfileHelper {
|
||||||
|
|
||||||
private final MessageReceiverProvider messageReceiverProvider;
|
private final MessageReceiverProvider messageReceiverProvider;
|
||||||
|
|
||||||
|
private final SignalServiceAddressResolver addressResolver;
|
||||||
|
|
||||||
public ProfileHelper(
|
public ProfileHelper(
|
||||||
final ProfileKeyProvider profileKeyProvider,
|
final ProfileKeyProvider profileKeyProvider,
|
||||||
final UnidentifiedAccessProvider unidentifiedAccessProvider,
|
final UnidentifiedAccessProvider unidentifiedAccessProvider,
|
||||||
final MessagePipeProvider messagePipeProvider,
|
final MessagePipeProvider messagePipeProvider,
|
||||||
final MessageReceiverProvider messageReceiverProvider
|
final MessageReceiverProvider messageReceiverProvider,
|
||||||
|
final SignalServiceAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
this.profileKeyProvider = profileKeyProvider;
|
this.profileKeyProvider = profileKeyProvider;
|
||||||
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
|
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
|
||||||
this.messagePipeProvider = messagePipeProvider;
|
this.messagePipeProvider = messagePipeProvider;
|
||||||
this.messageReceiverProvider = messageReceiverProvider;
|
this.messageReceiverProvider = messageReceiverProvider;
|
||||||
|
this.addressResolver = addressResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileAndCredential retrieveProfileSync(
|
public ProfileAndCredential retrieveProfileSync(
|
||||||
SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType
|
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
try {
|
try {
|
||||||
return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS);
|
return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
if (e.getCause() instanceof PushNetworkException) {
|
if (e.getCause() instanceof PushNetworkException) {
|
||||||
throw (PushNetworkException) e.getCause();
|
throw (PushNetworkException) e.getCause();
|
||||||
|
@ -58,11 +63,12 @@ public final class ProfileHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<ProfileAndCredential> retrieveProfile(
|
public ListenableFuture<ProfileAndCredential> retrieveProfile(
|
||||||
SignalServiceAddress address, SignalServiceProfile.RequestType requestType
|
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||||
) {
|
) {
|
||||||
var unidentifiedAccess = getUnidentifiedAccess(address);
|
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
||||||
var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address));
|
var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
|
||||||
|
|
||||||
|
final var address = addressResolver.resolveSignalServiceAddress(recipientId);
|
||||||
if (unidentifiedAccess.isPresent()) {
|
if (unidentifiedAccess.isPresent()) {
|
||||||
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
|
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -126,8 +132,8 @@ public final class ProfileHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
|
private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
|
||||||
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient);
|
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
|
||||||
|
|
||||||
if (unidentifiedAccess.isPresent()) {
|
if (unidentifiedAccess.isPresent()) {
|
||||||
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
public interface ProfileKeyCredentialProvider {
|
public interface ProfileKeyCredentialProvider {
|
||||||
|
|
||||||
ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address);
|
ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
public interface ProfileKeyProvider {
|
public interface ProfileKeyProvider {
|
||||||
|
|
||||||
ProfileKey getProfileKey(SignalServiceAddress address);
|
ProfileKey getProfileKey(RecipientId address);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
|
||||||
public interface ProfileProvider {
|
public interface ProfileProvider {
|
||||||
|
|
||||||
SignalProfile getProfile(SignalServiceAddress address);
|
Profile getProfile(RecipientId address);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package org.asamk.signal.manager.helper;
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
public interface SelfAddressProvider {
|
|
||||||
|
|
||||||
SignalServiceAddress getSelfAddress();
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
|
||||||
|
public interface SelfRecipientIdProvider {
|
||||||
|
|
||||||
|
RecipientId getSelfRecipientId();
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public interface SignalServiceAddressResolver {
|
||||||
|
|
||||||
|
SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId);
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -38,22 +38,25 @@ public class UnidentifiedAccessHelper {
|
||||||
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
|
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
|
public byte[] getTargetUnidentifiedAccessKey(RecipientId recipient) {
|
||||||
var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
|
|
||||||
if (theirProfileKey == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetProfile = profileProvider.getProfile(recipient);
|
var targetProfile = profileProvider.getProfile(recipient);
|
||||||
if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
|
if (targetProfile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
|
switch (targetProfile.getUnidentifiedAccessMode()) {
|
||||||
return createUnrestrictedUnidentifiedAccess();
|
case ENABLED:
|
||||||
}
|
var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
|
||||||
|
if (theirProfileKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||||
|
case UNRESTRICTED:
|
||||||
|
return createUnrestrictedUnidentifiedAccess();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<UnidentifiedAccessPair> getAccessForSync() {
|
public Optional<UnidentifiedAccessPair> getAccessForSync() {
|
||||||
|
@ -73,11 +76,11 @@ public class UnidentifiedAccessHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
|
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
|
||||||
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
|
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
|
public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
|
||||||
var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||||
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
|
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
|
||||||
var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
|
var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
public interface UnidentifiedAccessProvider {
|
public interface UnidentifiedAccessProvider {
|
||||||
|
|
||||||
Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress address);
|
Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipientId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,18 +10,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
import org.asamk.signal.manager.groups.GroupId;
|
import org.asamk.signal.manager.groups.GroupId;
|
||||||
import org.asamk.signal.manager.storage.contacts.ContactInfo;
|
import org.asamk.signal.manager.storage.contacts.ContactsStore;
|
||||||
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
|
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||||
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
|
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
||||||
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
||||||
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
|
||||||
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
|
||||||
|
import org.asamk.signal.manager.storage.profiles.LegacyProfileStore;
|
||||||
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
||||||
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
|
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
|
||||||
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
|
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.Contact;
|
||||||
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
||||||
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
||||||
|
@ -45,6 +48,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -57,9 +61,9 @@ import java.nio.channels.ClosedChannelException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class SignalAccount implements Closeable {
|
public class SignalAccount implements Closeable {
|
||||||
|
|
||||||
|
@ -88,9 +92,7 @@ public class SignalAccount implements Closeable {
|
||||||
private SessionStore sessionStore;
|
private SessionStore sessionStore;
|
||||||
private IdentityKeyStore identityKeyStore;
|
private IdentityKeyStore identityKeyStore;
|
||||||
private JsonGroupStore groupStore;
|
private JsonGroupStore groupStore;
|
||||||
private JsonContactsStore contactStore;
|
|
||||||
private RecipientStore recipientStore;
|
private RecipientStore recipientStore;
|
||||||
private ProfileStore profileStore;
|
|
||||||
private StickerStore stickerStore;
|
private StickerStore stickerStore;
|
||||||
|
|
||||||
private MessageCache messageCache;
|
private MessageCache messageCache;
|
||||||
|
@ -136,7 +138,6 @@ public class SignalAccount implements Closeable {
|
||||||
account.username = username;
|
account.username = username;
|
||||||
account.profileKey = profileKey;
|
account.profileKey = profileKey;
|
||||||
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||||
account.contactStore = new JsonContactsStore();
|
|
||||||
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
|
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
|
||||||
account::mergeRecipients);
|
account::mergeRecipients);
|
||||||
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
||||||
|
@ -151,7 +152,6 @@ public class SignalAccount implements Closeable {
|
||||||
account.signedPreKeyStore,
|
account.signedPreKeyStore,
|
||||||
account.sessionStore,
|
account.sessionStore,
|
||||||
account.identityKeyStore);
|
account.identityKeyStore);
|
||||||
account.profileStore = new ProfileStore();
|
|
||||||
account.stickerStore = new StickerStore();
|
account.stickerStore = new StickerStore();
|
||||||
|
|
||||||
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||||
|
@ -188,9 +188,9 @@ public class SignalAccount implements Closeable {
|
||||||
account.profileKey = profileKey;
|
account.profileKey = profileKey;
|
||||||
account.deviceId = deviceId;
|
account.deviceId = deviceId;
|
||||||
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||||
account.contactStore = new JsonContactsStore();
|
|
||||||
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
|
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
|
||||||
account::mergeRecipients);
|
account::mergeRecipients);
|
||||||
|
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
|
||||||
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
|
||||||
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
|
||||||
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
|
||||||
|
@ -203,7 +203,6 @@ public class SignalAccount implements Closeable {
|
||||||
account.signedPreKeyStore,
|
account.signedPreKeyStore,
|
||||||
account.sessionStore,
|
account.sessionStore,
|
||||||
account.identityKeyStore);
|
account.identityKeyStore);
|
||||||
account.profileStore = new ProfileStore();
|
|
||||||
account.stickerStore = new StickerStore();
|
account.stickerStore = new StickerStore();
|
||||||
|
|
||||||
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||||
|
@ -222,23 +221,8 @@ public class SignalAccount implements Closeable {
|
||||||
setProfileKey(KeyUtils.createProfileKey());
|
setProfileKey(KeyUtils.createProfileKey());
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
// Store profile keys only in profile store
|
|
||||||
for (var contact : getContactStore().getContacts()) {
|
|
||||||
var profileKeyString = contact.profileKey;
|
|
||||||
if (profileKeyString == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final ProfileKey profileKey;
|
|
||||||
try {
|
|
||||||
profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
|
|
||||||
} catch (InvalidInputException ignored) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
contact.profileKey = null;
|
|
||||||
getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
|
|
||||||
}
|
|
||||||
// Ensure our profile key is stored in profile store
|
// Ensure our profile key is stored in profile store
|
||||||
getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
|
getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
|
@ -354,13 +338,15 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
|
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
|
||||||
|
|
||||||
var legacyRecipientStoreNode = rootNode.get("recipientStore");
|
var legacyRecipientStoreNode = rootNode.get("recipientStore");
|
||||||
if (legacyRecipientStoreNode != null) {
|
if (legacyRecipientStoreNode != null) {
|
||||||
logger.debug("Migrating legacy recipient store.");
|
logger.debug("Migrating legacy recipient store.");
|
||||||
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
|
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
|
||||||
if (legacyRecipientStore != null) {
|
if (legacyRecipientStore != null) {
|
||||||
recipientStore.resolveRecipients(legacyRecipientStore.getAddresses());
|
recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
|
||||||
}
|
}
|
||||||
|
recipientStore.resolveRecipientTrusted(getSelfAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
|
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
|
||||||
|
@ -414,9 +400,9 @@ public class SignalAccount implements Closeable {
|
||||||
identityKeyPair,
|
identityKeyPair,
|
||||||
registrationId);
|
registrationId);
|
||||||
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
|
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
|
||||||
logger.debug("Migrating identity session store.");
|
logger.debug("Migrating legacy identity session store.");
|
||||||
for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
|
for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
|
||||||
RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
|
RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
|
||||||
identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
|
identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
|
||||||
identityKeyStore.setIdentityTrustLevel(recipientId,
|
identityKeyStore.setIdentityTrustLevel(recipientId,
|
||||||
identity.getIdentityKey(),
|
identity.getIdentityKey(),
|
||||||
|
@ -436,20 +422,67 @@ public class SignalAccount implements Closeable {
|
||||||
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||||
}
|
}
|
||||||
|
|
||||||
var contactStoreNode = rootNode.get("contactStore");
|
if (rootNode.hasNonNull("contactStore")) {
|
||||||
if (contactStoreNode != null) {
|
logger.debug("Migrating legacy contact store.");
|
||||||
contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
|
final var contactStoreNode = rootNode.get("contactStore");
|
||||||
}
|
final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
|
||||||
if (contactStore == null) {
|
for (var contact : contactStore.getContacts()) {
|
||||||
contactStore = new JsonContactsStore();
|
final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
|
||||||
|
recipientStore.storeContact(recipientId,
|
||||||
|
new Contact(contact.name,
|
||||||
|
contact.color,
|
||||||
|
contact.messageExpirationTime,
|
||||||
|
contact.blocked,
|
||||||
|
contact.archived));
|
||||||
|
|
||||||
|
// Store profile keys only in profile store
|
||||||
|
var profileKeyString = contact.profileKey;
|
||||||
|
if (profileKeyString != null) {
|
||||||
|
final ProfileKey profileKey;
|
||||||
|
try {
|
||||||
|
profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
|
||||||
|
getProfileStore().storeProfileKey(recipientId, profileKey);
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileStoreNode = rootNode.get("profileStore");
|
if (rootNode.hasNonNull("profileStore")) {
|
||||||
if (profileStoreNode != null) {
|
logger.debug("Migrating legacy profile store.");
|
||||||
profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
|
var profileStoreNode = rootNode.get("profileStore");
|
||||||
}
|
final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
|
||||||
if (profileStore == null) {
|
for (var profileEntry : legacyProfileStore.getProfileEntries()) {
|
||||||
profileStore = new ProfileStore();
|
var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress());
|
||||||
|
recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
|
||||||
|
recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
|
||||||
|
final var profile = profileEntry.getProfile();
|
||||||
|
if (profile != null) {
|
||||||
|
final var capabilities = new HashSet<Profile.Capability>();
|
||||||
|
if (profile.getCapabilities().gv1Migration) {
|
||||||
|
capabilities.add(Profile.Capability.gv1Migration);
|
||||||
|
}
|
||||||
|
if (profile.getCapabilities().gv2) {
|
||||||
|
capabilities.add(Profile.Capability.gv2);
|
||||||
|
}
|
||||||
|
if (profile.getCapabilities().storage) {
|
||||||
|
capabilities.add(Profile.Capability.storage);
|
||||||
|
}
|
||||||
|
final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
|
||||||
|
profile.getGivenName(),
|
||||||
|
profile.getFamilyName(),
|
||||||
|
profile.getAbout(),
|
||||||
|
profile.getAboutEmoji(),
|
||||||
|
profile.isUnrestrictedUnidentifiedAccess()
|
||||||
|
? Profile.UnidentifiedAccessMode.UNRESTRICTED
|
||||||
|
: profile.getUnidentifiedAccess() != null
|
||||||
|
? Profile.UnidentifiedAccessMode.ENABLED
|
||||||
|
: Profile.UnidentifiedAccessMode.DISABLED,
|
||||||
|
capabilities);
|
||||||
|
recipientStore.storeProfile(recipientId, newProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var stickerStoreNode = rootNode.get("stickerStore");
|
var stickerStoreNode = rootNode.get("stickerStore");
|
||||||
|
@ -460,24 +493,6 @@ public class SignalAccount implements Closeable {
|
||||||
stickerStore = new StickerStore();
|
stickerStore = new StickerStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipientStore.isEmpty()) {
|
|
||||||
recipientStore.resolveRecipient(getSelfAddress());
|
|
||||||
|
|
||||||
recipientStore.resolveRecipients(contactStore.getContacts()
|
|
||||||
.stream()
|
|
||||||
.map(ContactInfo::getAddress)
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
|
|
||||||
for (var group : groupStore.getGroups()) {
|
|
||||||
if (group instanceof GroupInfoV1) {
|
|
||||||
var groupInfoV1 = (GroupInfoV1) group;
|
|
||||||
groupInfoV1.members = groupInfoV1.members.stream()
|
|
||||||
.map(m -> recipientStore.resolveServiceAddress(m))
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
|
||||||
|
|
||||||
var threadStoreNode = rootNode.get("threadStore");
|
var threadStoreNode = rootNode.get("threadStore");
|
||||||
|
@ -489,10 +504,15 @@ public class SignalAccount implements Closeable {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
|
if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
|
||||||
if (contactInfo != null) {
|
final var recipientId = recipientStore.resolveRecipient(thread.id);
|
||||||
contactInfo.messageExpirationTime = thread.messageExpirationTime;
|
var contact = recipientStore.getContact(recipientId);
|
||||||
contactStore.updateContact(contactInfo);
|
if (contact != null) {
|
||||||
|
recipientStore.storeContact(recipientId,
|
||||||
|
Contact.newBuilder(contact)
|
||||||
|
.withMessageExpirationTime(thread.messageExpirationTime)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
|
var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
|
||||||
if (groupInfo instanceof GroupInfoV1) {
|
if (groupInfo instanceof GroupInfoV1) {
|
||||||
|
@ -500,7 +520,8 @@ public class SignalAccount implements Closeable {
|
||||||
groupStore.updateGroup(groupInfo);
|
groupStore.updateGroup(groupInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to read legacy thread info: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,8 +554,6 @@ public class SignalAccount implements Closeable {
|
||||||
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
|
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
|
||||||
.put("registered", registered)
|
.put("registered", registered)
|
||||||
.putPOJO("groupStore", groupStore)
|
.putPOJO("groupStore", groupStore)
|
||||||
.putPOJO("contactStore", contactStore)
|
|
||||||
.putPOJO("profileStore", profileStore)
|
|
||||||
.putPOJO("stickerStore", stickerStore);
|
.putPOJO("stickerStore", stickerStore);
|
||||||
try {
|
try {
|
||||||
try (var output = new ByteArrayOutputStream()) {
|
try (var output = new ByteArrayOutputStream()) {
|
||||||
|
@ -602,8 +621,8 @@ public class SignalAccount implements Closeable {
|
||||||
return groupStore;
|
return groupStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonContactsStore getContactStore() {
|
public ContactsStore getContactStore() {
|
||||||
return contactStore;
|
return recipientStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientStore getRecipientStore() {
|
public RecipientStore getRecipientStore() {
|
||||||
|
@ -611,7 +630,7 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileStore getProfileStore() {
|
public ProfileStore getProfileStore() {
|
||||||
return profileStore;
|
return recipientStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StickerStore getStickerStore() {
|
public StickerStore getStickerStore() {
|
||||||
|
@ -638,6 +657,10 @@ public class SignalAccount implements Closeable {
|
||||||
return new SignalServiceAddress(uuid, username);
|
return new SignalServiceAddress(uuid, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RecipientId getSelfRecipientId() {
|
||||||
|
return recipientStore.resolveRecipientTrusted(getSelfAddress());
|
||||||
|
}
|
||||||
|
|
||||||
public int getDeviceId() {
|
public int getDeviceId() {
|
||||||
return deviceId;
|
return deviceId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.asamk.signal.manager.storage.contacts;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.recipients.Contact;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ContactsStore {
|
||||||
|
|
||||||
|
void storeContact(RecipientId recipientId, Contact contact);
|
||||||
|
|
||||||
|
Contact getContact(RecipientId recipientId);
|
||||||
|
|
||||||
|
List<Pair<RecipientId, Contact>> getContacts();
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
package org.asamk.signal.manager.storage.contacts;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class JsonContactsStore {
|
|
||||||
|
|
||||||
@JsonProperty("contacts")
|
|
||||||
private List<ContactInfo> contacts = new ArrayList<>();
|
|
||||||
|
|
||||||
public void updateContact(ContactInfo contact) {
|
|
||||||
final var contactAddress = contact.getAddress();
|
|
||||||
for (var i = 0; i < contacts.size(); i++) {
|
|
||||||
if (contacts.get(i).getAddress().matches(contactAddress)) {
|
|
||||||
contacts.set(i, contact);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts.add(contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContactInfo getContact(SignalServiceAddress address) {
|
|
||||||
for (var contact : contacts) {
|
|
||||||
if (contact.getAddress().matches(address)) {
|
|
||||||
if (contact.uuid == null) {
|
|
||||||
contact.uuid = address.getUuid().orNull();
|
|
||||||
} else if (contact.number == null) {
|
|
||||||
contact.number = address.getNumber().orNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
return contact;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ContactInfo> getContacts() {
|
|
||||||
return new ArrayList<>(contacts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all contacts from the store
|
|
||||||
*/
|
|
||||||
public void clear() {
|
|
||||||
contacts.clear();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ import java.util.UUID;
|
||||||
|
|
||||||
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
|
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
|
||||||
|
|
||||||
public class ContactInfo {
|
public class LegacyContactInfo {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
public String name;
|
public String name;
|
||||||
|
@ -38,12 +38,7 @@ public class ContactInfo {
|
||||||
@JsonProperty(defaultValue = "false")
|
@JsonProperty(defaultValue = "false")
|
||||||
public boolean archived;
|
public boolean archived;
|
||||||
|
|
||||||
public ContactInfo() {
|
public LegacyContactInfo() {
|
||||||
}
|
|
||||||
|
|
||||||
public ContactInfo(SignalServiceAddress address) {
|
|
||||||
this.number = address.getNumber().orNull();
|
|
||||||
this.uuid = address.getUuid().orNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.asamk.signal.manager.storage.contacts;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyJsonContactsStore {
|
||||||
|
|
||||||
|
@JsonProperty("contacts")
|
||||||
|
private final List<LegacyContactInfo> contacts = new ArrayList<>();
|
||||||
|
|
||||||
|
private LegacyJsonContactsStore() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LegacyContactInfo> getContacts() {
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package org.asamk.signal.manager.storage.profiles;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
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.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyProfileStore {
|
||||||
|
|
||||||
|
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||||
|
|
||||||
|
@JsonProperty("profiles")
|
||||||
|
@JsonDeserialize(using = ProfileStoreDeserializer.class)
|
||||||
|
private final List<LegacySignalProfileEntry> profiles = new ArrayList<>();
|
||||||
|
|
||||||
|
public List<LegacySignalProfileEntry> getProfileEntries() {
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProfileStoreDeserializer extends JsonDeserializer<List<LegacySignalProfileEntry>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LegacySignalProfileEntry> deserialize(
|
||||||
|
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||||
|
) throws IOException {
|
||||||
|
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||||
|
|
||||||
|
var profileEntries = new ArrayList<LegacySignalProfileEntry>();
|
||||||
|
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (var entry : node) {
|
||||||
|
var name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
|
||||||
|
var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
|
||||||
|
final var serviceAddress = new SignalServiceAddress(uuid, name);
|
||||||
|
ProfileKey profileKey = null;
|
||||||
|
try {
|
||||||
|
profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText()));
|
||||||
|
} catch (InvalidInputException ignored) {
|
||||||
|
}
|
||||||
|
ProfileKeyCredential profileKeyCredential = null;
|
||||||
|
if (entry.hasNonNull("profileKeyCredential")) {
|
||||||
|
try {
|
||||||
|
profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
|
||||||
|
.decode(entry.get("profileKeyCredential").asText()));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
|
||||||
|
var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
|
||||||
|
profileEntries.add(new LegacySignalProfileEntry(serviceAddress,
|
||||||
|
profileKey,
|
||||||
|
lastUpdateTimestamp,
|
||||||
|
profile,
|
||||||
|
profileKeyCredential));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
public class SignalProfileEntry {
|
public class LegacySignalProfileEntry {
|
||||||
|
|
||||||
private final SignalServiceAddress serviceAddress;
|
private final SignalServiceAddress serviceAddress;
|
||||||
|
|
||||||
|
@ -16,9 +16,7 @@ public class SignalProfileEntry {
|
||||||
|
|
||||||
private final ProfileKeyCredential profileKeyCredential;
|
private final ProfileKeyCredential profileKeyCredential;
|
||||||
|
|
||||||
private boolean requestPending;
|
public LegacySignalProfileEntry(
|
||||||
|
|
||||||
public SignalProfileEntry(
|
|
||||||
final SignalServiceAddress serviceAddress,
|
final SignalServiceAddress serviceAddress,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final long lastUpdateTimestamp,
|
final long lastUpdateTimestamp,
|
||||||
|
@ -51,12 +49,4 @@ public class SignalProfileEntry {
|
||||||
public ProfileKeyCredential getProfileKeyCredential() {
|
public ProfileKeyCredential getProfileKeyCredential() {
|
||||||
return profileKeyCredential;
|
return profileKeyCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRequestPending() {
|
|
||||||
return requestPending;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRequestPending(final boolean requestPending) {
|
|
||||||
this.requestPending = requestPending;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,156 +1,21 @@
|
||||||
package org.asamk.signal.manager.storage.profiles;
|
package org.asamk.signal.manager.storage.profiles;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
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.signal.zkgroup.InvalidInputException;
|
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
public interface ProfileStore {
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ProfileStore {
|
Profile getProfile(RecipientId recipientId);
|
||||||
|
|
||||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
ProfileKey getProfileKey(RecipientId recipientId);
|
||||||
|
|
||||||
@JsonProperty("profiles")
|
ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
|
||||||
@JsonDeserialize(using = ProfileStoreDeserializer.class)
|
|
||||||
@JsonSerialize(using = ProfileStoreSerializer.class)
|
|
||||||
private final List<SignalProfileEntry> profiles = new ArrayList<>();
|
|
||||||
|
|
||||||
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
|
void storeProfile(RecipientId recipientId, Profile profile);
|
||||||
for (var entry : profiles) {
|
|
||||||
if (entry.getServiceAddress().matches(serviceAddress)) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) {
|
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
|
||||||
for (var entry : profiles) {
|
|
||||||
if (entry.getServiceAddress().matches(serviceAddress)) {
|
|
||||||
return entry.getProfileKey();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateProfile(
|
void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);
|
||||||
SignalServiceAddress serviceAddress,
|
|
||||||
ProfileKey profileKey,
|
|
||||||
long now,
|
|
||||||
SignalProfile profile,
|
|
||||||
ProfileKeyCredential profileKeyCredential
|
|
||||||
) {
|
|
||||||
var newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile, profileKeyCredential);
|
|
||||||
for (var i = 0; i < profiles.size(); i++) {
|
|
||||||
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
|
||||||
profiles.set(i, newEntry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles.add(newEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
|
|
||||||
var newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null);
|
|
||||||
for (var i = 0; i < profiles.size(); i++) {
|
|
||||||
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
|
||||||
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
|
|
||||||
profiles.set(i, newEntry);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles.add(newEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<SignalProfileEntry> deserialize(
|
|
||||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
|
||||||
) throws IOException {
|
|
||||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
|
||||||
|
|
||||||
var addresses = new ArrayList<SignalProfileEntry>();
|
|
||||||
|
|
||||||
if (node.isArray()) {
|
|
||||||
for (var entry : node) {
|
|
||||||
var name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
|
|
||||||
var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
|
|
||||||
final var serviceAddress = new SignalServiceAddress(uuid, name);
|
|
||||||
ProfileKey profileKey = null;
|
|
||||||
try {
|
|
||||||
profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText()));
|
|
||||||
} catch (InvalidInputException ignored) {
|
|
||||||
}
|
|
||||||
ProfileKeyCredential profileKeyCredential = null;
|
|
||||||
if (entry.hasNonNull("profileKeyCredential")) {
|
|
||||||
try {
|
|
||||||
profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
|
|
||||||
.decode(entry.get("profileKeyCredential").asText()));
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
|
|
||||||
var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
|
|
||||||
addresses.add(new SignalProfileEntry(serviceAddress,
|
|
||||||
profileKey,
|
|
||||||
lastUpdateTimestamp,
|
|
||||||
profile,
|
|
||||||
profileKeyCredential));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serialize(
|
|
||||||
List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider
|
|
||||||
) throws IOException {
|
|
||||||
json.writeStartArray();
|
|
||||||
for (var profileEntry : profiles) {
|
|
||||||
final var address = profileEntry.getServiceAddress();
|
|
||||||
json.writeStartObject();
|
|
||||||
if (address.getNumber().isPresent()) {
|
|
||||||
json.writeStringField("name", address.getNumber().get());
|
|
||||||
}
|
|
||||||
if (address.getUuid().isPresent()) {
|
|
||||||
json.writeStringField("uuid", address.getUuid().get().toString());
|
|
||||||
}
|
|
||||||
json.writeStringField("profileKey",
|
|
||||||
Base64.getEncoder().encodeToString(profileEntry.getProfileKey().serialize()));
|
|
||||||
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
|
|
||||||
json.writeObjectField("profile", profileEntry.getProfile());
|
|
||||||
if (profileEntry.getProfileKeyCredential() != null) {
|
|
||||||
json.writeStringField("profileKeyCredential",
|
|
||||||
Base64.getEncoder().encodeToString(profileEntry.getProfileKeyCredential().serialize()));
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
json.writeEndArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,11 @@ package org.asamk.signal.manager.storage.profiles;
|
||||||
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 org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
|
||||||
|
|
||||||
public class SignalProfile {
|
public class SignalProfile {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private final String identityKey;
|
@JsonIgnore
|
||||||
|
private String identityKey;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -29,28 +28,6 @@ public class SignalProfile {
|
||||||
private final Capabilities capabilities;
|
private final Capabilities capabilities;
|
||||||
|
|
||||||
public SignalProfile(
|
public SignalProfile(
|
||||||
final String identityKey,
|
|
||||||
final String name,
|
|
||||||
final String about,
|
|
||||||
final String aboutEmoji,
|
|
||||||
final String unidentifiedAccess,
|
|
||||||
final boolean unrestrictedUnidentifiedAccess,
|
|
||||||
final SignalServiceProfile.Capabilities capabilities
|
|
||||||
) {
|
|
||||||
this.identityKey = identityKey;
|
|
||||||
this.name = name;
|
|
||||||
this.about = about;
|
|
||||||
this.aboutEmoji = aboutEmoji;
|
|
||||||
this.unidentifiedAccess = unidentifiedAccess;
|
|
||||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
|
||||||
this.capabilities = new Capabilities();
|
|
||||||
this.capabilities.storage = capabilities.isStorage();
|
|
||||||
this.capabilities.gv1Migration = capabilities.isGv1Migration();
|
|
||||||
this.capabilities.gv2 = capabilities.isGv2();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalProfile(
|
|
||||||
@JsonProperty("identityKey") final String identityKey,
|
|
||||||
@JsonProperty("name") final String name,
|
@JsonProperty("name") final String name,
|
||||||
@JsonProperty("about") final String about,
|
@JsonProperty("about") final String about,
|
||||||
@JsonProperty("aboutEmoji") final String aboutEmoji,
|
@JsonProperty("aboutEmoji") final String aboutEmoji,
|
||||||
|
@ -58,7 +35,6 @@ public class SignalProfile {
|
||||||
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
|
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
|
||||||
@JsonProperty("capabilities") final Capabilities capabilities
|
@JsonProperty("capabilities") final Capabilities capabilities
|
||||||
) {
|
) {
|
||||||
this.identityKey = identityKey;
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.about = about;
|
this.about = about;
|
||||||
this.aboutEmoji = aboutEmoji;
|
this.aboutEmoji = aboutEmoji;
|
||||||
|
@ -67,17 +43,24 @@ public class SignalProfile {
|
||||||
this.capabilities = capabilities;
|
this.capabilities = capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIdentityKey() {
|
public String getGivenName() {
|
||||||
return identityKey;
|
if (name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = name.split("\0");
|
||||||
|
|
||||||
|
return parts.length < 1 ? null : parts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getFamilyName() {
|
||||||
return name;
|
if (name == null) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDisplayName() {
|
String[] parts = name.split("\0");
|
||||||
// First name and last name (if set) are separated by a NULL char + trim space in case only one is filled
|
|
||||||
return name == null ? "" : name.replace("\0", " ").trim();
|
return parts.length < 2 ? null : parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAbout() {
|
public String getAbout() {
|
||||||
|
@ -100,31 +83,6 @@ public class SignalProfile {
|
||||||
return capabilities;
|
return capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "SignalProfile{"
|
|
||||||
+ "identityKey='"
|
|
||||||
+ identityKey
|
|
||||||
+ '\''
|
|
||||||
+ ", name='"
|
|
||||||
+ name
|
|
||||||
+ '\''
|
|
||||||
+ ", about='"
|
|
||||||
+ about
|
|
||||||
+ '\''
|
|
||||||
+ ", aboutEmoji='"
|
|
||||||
+ aboutEmoji
|
|
||||||
+ '\''
|
|
||||||
+ ", unidentifiedAccess='"
|
|
||||||
+ unidentifiedAccess
|
|
||||||
+ '\''
|
|
||||||
+ ", unrestrictedUnidentifiedAccess="
|
|
||||||
+ unrestrictedUnidentifiedAccess
|
|
||||||
+ ", capabilities="
|
|
||||||
+ capabilities
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Capabilities {
|
public static class Capabilities {
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
public class Contact {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final String color;
|
||||||
|
|
||||||
|
private final int messageExpirationTime;
|
||||||
|
|
||||||
|
private final boolean blocked;
|
||||||
|
|
||||||
|
private final boolean archived;
|
||||||
|
|
||||||
|
public Contact(
|
||||||
|
final String name,
|
||||||
|
final String color,
|
||||||
|
final int messageExpirationTime,
|
||||||
|
final boolean blocked,
|
||||||
|
final boolean archived
|
||||||
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.messageExpirationTime = messageExpirationTime;
|
||||||
|
this.blocked = blocked;
|
||||||
|
this.archived = archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Contact(final Builder builder) {
|
||||||
|
name = builder.name;
|
||||||
|
color = builder.color;
|
||||||
|
messageExpirationTime = builder.messageExpirationTime;
|
||||||
|
blocked = builder.blocked;
|
||||||
|
archived = builder.archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder(final Contact copy) {
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.name = copy.getName();
|
||||||
|
builder.color = copy.getColor();
|
||||||
|
builder.messageExpirationTime = copy.getMessageExpirationTime();
|
||||||
|
builder.blocked = copy.isBlocked();
|
||||||
|
builder.archived = copy.isArchived();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMessageExpirationTime() {
|
||||||
|
return messageExpirationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked() {
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isArchived() {
|
||||||
|
return archived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String color;
|
||||||
|
private int messageExpirationTime;
|
||||||
|
private boolean blocked;
|
||||||
|
private boolean archived;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withName(final String val) {
|
||||||
|
name = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withColor(final String val) {
|
||||||
|
color = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withMessageExpirationTime(final int val) {
|
||||||
|
messageExpirationTime = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withBlocked(final boolean val) {
|
||||||
|
blocked = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArchived(final boolean val) {
|
||||||
|
archived = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Contact build() {
|
||||||
|
return new Contact(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Profile {
|
||||||
|
|
||||||
|
private final long lastUpdateTimestamp;
|
||||||
|
|
||||||
|
private final String givenName;
|
||||||
|
|
||||||
|
private final String familyName;
|
||||||
|
|
||||||
|
private final String about;
|
||||||
|
|
||||||
|
private final String aboutEmoji;
|
||||||
|
|
||||||
|
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||||
|
|
||||||
|
private final Set<Capability> capabilities;
|
||||||
|
|
||||||
|
public Profile(
|
||||||
|
final long lastUpdateTimestamp,
|
||||||
|
final String givenName,
|
||||||
|
final String familyName,
|
||||||
|
final String about,
|
||||||
|
final String aboutEmoji,
|
||||||
|
final UnidentifiedAccessMode unidentifiedAccessMode,
|
||||||
|
final Set<Capability> capabilities
|
||||||
|
) {
|
||||||
|
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
||||||
|
this.givenName = givenName;
|
||||||
|
this.familyName = familyName;
|
||||||
|
this.about = about;
|
||||||
|
this.aboutEmoji = aboutEmoji;
|
||||||
|
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Profile(final Builder builder) {
|
||||||
|
lastUpdateTimestamp = builder.lastUpdateTimestamp;
|
||||||
|
givenName = builder.givenName;
|
||||||
|
familyName = builder.familyName;
|
||||||
|
about = builder.about;
|
||||||
|
aboutEmoji = builder.aboutEmoji;
|
||||||
|
unidentifiedAccessMode = builder.unidentifiedAccessMode;
|
||||||
|
capabilities = builder.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder(final Profile copy) {
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.lastUpdateTimestamp = copy.getLastUpdateTimestamp();
|
||||||
|
builder.givenName = copy.getGivenName();
|
||||||
|
builder.familyName = copy.getFamilyName();
|
||||||
|
builder.about = copy.getAbout();
|
||||||
|
builder.aboutEmoji = copy.getAboutEmoji();
|
||||||
|
builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
|
||||||
|
builder.capabilities = copy.getCapabilities();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastUpdateTimestamp() {
|
||||||
|
return lastUpdateTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGivenName() {
|
||||||
|
return givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFamilyName() {
|
||||||
|
return familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInternalServiceName() {
|
||||||
|
if (familyName == null) {
|
||||||
|
return givenName == null ? "" : givenName;
|
||||||
|
}
|
||||||
|
return String.join("\0", givenName == null ? "" : givenName, familyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
final var noGivenName = Util.isEmpty(givenName);
|
||||||
|
final var noFamilyName = Util.isEmpty(familyName);
|
||||||
|
|
||||||
|
if (noGivenName && noFamilyName) {
|
||||||
|
return "";
|
||||||
|
} else if (noGivenName) {
|
||||||
|
return familyName;
|
||||||
|
} else if (noFamilyName) {
|
||||||
|
return givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return givenName + " " + familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAbout() {
|
||||||
|
return about;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAboutEmoji() {
|
||||||
|
return aboutEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnidentifiedAccessMode getUnidentifiedAccessMode() {
|
||||||
|
return unidentifiedAccessMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Capability> getCapabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UnidentifiedAccessMode {
|
||||||
|
UNKNOWN,
|
||||||
|
DISABLED,
|
||||||
|
ENABLED,
|
||||||
|
UNRESTRICTED;
|
||||||
|
|
||||||
|
static UnidentifiedAccessMode valueOfOrUnknown(String value) {
|
||||||
|
try {
|
||||||
|
return valueOf(value);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Capability {
|
||||||
|
gv2,
|
||||||
|
storage,
|
||||||
|
gv1Migration;
|
||||||
|
|
||||||
|
static Capability valueOfOrNull(String value) {
|
||||||
|
try {
|
||||||
|
return valueOf(value);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private String givenName;
|
||||||
|
private String familyName;
|
||||||
|
private String about;
|
||||||
|
private String aboutEmoji;
|
||||||
|
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
||||||
|
private Set<Capability> capabilities = Collections.emptySet();
|
||||||
|
private long lastUpdateTimestamp = 0;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withGivenName(final String val) {
|
||||||
|
givenName = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withFamilyName(final String val) {
|
||||||
|
familyName = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAbout(final String val) {
|
||||||
|
about = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAboutEmoji(final String val) {
|
||||||
|
aboutEmoji = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) {
|
||||||
|
unidentifiedAccessMode = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withCapabilities(final Set<Capability> val) {
|
||||||
|
capabilities = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profile build() {
|
||||||
|
return new Profile(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withLastUpdateTimestamp(final long val) {
|
||||||
|
lastUpdateTimestamp = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class Recipient {
|
||||||
|
|
||||||
|
private final RecipientId recipientId;
|
||||||
|
|
||||||
|
private final SignalServiceAddress address;
|
||||||
|
|
||||||
|
private final Contact contact;
|
||||||
|
|
||||||
|
private final ProfileKey profileKey;
|
||||||
|
|
||||||
|
private final ProfileKeyCredential profileKeyCredential;
|
||||||
|
|
||||||
|
private final Profile profile;
|
||||||
|
|
||||||
|
public Recipient(
|
||||||
|
final RecipientId recipientId,
|
||||||
|
final SignalServiceAddress address,
|
||||||
|
final Contact contact,
|
||||||
|
final ProfileKey profileKey,
|
||||||
|
final ProfileKeyCredential profileKeyCredential,
|
||||||
|
final Profile profile
|
||||||
|
) {
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.address = address;
|
||||||
|
this.contact = contact;
|
||||||
|
this.profileKey = profileKey;
|
||||||
|
this.profileKeyCredential = profileKeyCredential;
|
||||||
|
this.profile = profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipient(final Builder builder) {
|
||||||
|
recipientId = builder.recipientId;
|
||||||
|
address = builder.address;
|
||||||
|
contact = builder.contact;
|
||||||
|
profileKey = builder.profileKey;
|
||||||
|
profileKeyCredential = builder.profileKeyCredential;
|
||||||
|
profile = builder.profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder(final Recipient copy) {
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.recipientId = copy.getRecipientId();
|
||||||
|
builder.address = copy.getAddress();
|
||||||
|
builder.contact = copy.getContact();
|
||||||
|
builder.profileKey = copy.getProfileKey();
|
||||||
|
builder.profileKeyCredential = copy.getProfileKeyCredential();
|
||||||
|
builder.profile = copy.getProfile();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecipientId getRecipientId() {
|
||||||
|
return recipientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Contact getContact() {
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProfileKey getProfileKey() {
|
||||||
|
return profileKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProfileKeyCredential getProfileKeyCredential() {
|
||||||
|
return profileKeyCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profile getProfile() {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
private RecipientId recipientId;
|
||||||
|
private SignalServiceAddress address;
|
||||||
|
private Contact contact;
|
||||||
|
private ProfileKey profileKey;
|
||||||
|
private ProfileKeyCredential profileKeyCredential;
|
||||||
|
private Profile profile;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withRecipientId(final RecipientId val) {
|
||||||
|
recipientId = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAddress(final SignalServiceAddress val) {
|
||||||
|
address = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withContact(final Contact val) {
|
||||||
|
contact = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withProfileKey(final ProfileKey val) {
|
||||||
|
profileKey = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withProfileKeyCredential(final ProfileKeyCredential val) {
|
||||||
|
profileKeyCredential = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withProfile(final Profile val) {
|
||||||
|
profile = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recipient build() {
|
||||||
|
return new Recipient(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,11 @@ package org.asamk.signal.manager.storage.recipients;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.Utils;
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
|
import org.asamk.signal.manager.storage.contacts.ContactsStore;
|
||||||
|
import org.asamk.signal.manager.storage.profiles.ProfileStore;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
@ -17,14 +22,17 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class RecipientStore {
|
public class RecipientStore implements ContactsStore, ProfileStore {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
|
private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
|
||||||
|
|
||||||
|
@ -32,7 +40,7 @@ public class RecipientStore {
|
||||||
private final File file;
|
private final File file;
|
||||||
private final RecipientMergeHandler recipientMergeHandler;
|
private final RecipientMergeHandler recipientMergeHandler;
|
||||||
|
|
||||||
private final Map<RecipientId, SignalServiceAddress> recipients;
|
private final Map<RecipientId, Recipient> recipients;
|
||||||
private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>();
|
private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>();
|
||||||
|
|
||||||
private long lastId;
|
private long lastId;
|
||||||
|
@ -40,16 +48,57 @@ public class RecipientStore {
|
||||||
public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
|
public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
|
||||||
final var objectMapper = Utils.createStorageObjectMapper();
|
final var objectMapper = Utils.createStorageObjectMapper();
|
||||||
try (var inputStream = new FileInputStream(file)) {
|
try (var inputStream = new FileInputStream(file)) {
|
||||||
var storage = objectMapper.readValue(inputStream, Storage.class);
|
final var storage = objectMapper.readValue(inputStream, Storage.class);
|
||||||
return new RecipientStore(objectMapper,
|
final var recipients = storage.recipients.stream().map(r -> {
|
||||||
file,
|
final var recipientId = new RecipientId(r.id);
|
||||||
recipientMergeHandler,
|
final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
|
||||||
storage.recipients.stream()
|
r.uuid).transform(UuidUtil::parseOrThrow),
|
||||||
.collect(Collectors.toMap(r -> new RecipientId(r.id),
|
org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number));
|
||||||
r -> new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
|
|
||||||
r.uuid).transform(UuidUtil::parseOrThrow),
|
Contact contact = null;
|
||||||
org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.name)))),
|
if (r.contact != null) {
|
||||||
storage.lastId);
|
contact = new Contact(r.contact.name,
|
||||||
|
r.contact.color,
|
||||||
|
r.contact.messageExpirationTime,
|
||||||
|
r.contact.blocked,
|
||||||
|
r.contact.archived);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileKey profileKey = null;
|
||||||
|
if (r.profileKey != null) {
|
||||||
|
try {
|
||||||
|
profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
|
||||||
|
} catch (InvalidInputException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileKeyCredential profileKeyCredential = null;
|
||||||
|
if (r.profileKeyCredential != null) {
|
||||||
|
try {
|
||||||
|
profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
|
||||||
|
.decode(r.profileKeyCredential));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile profile = null;
|
||||||
|
if (r.profile != null) {
|
||||||
|
profile = new Profile(r.profile.lastUpdateTimestamp,
|
||||||
|
r.profile.givenName,
|
||||||
|
r.profile.familyName,
|
||||||
|
r.profile.about,
|
||||||
|
r.profile.aboutEmoji,
|
||||||
|
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
|
||||||
|
r.profile.capabilities.stream()
|
||||||
|
.map(Profile.Capability::valueOfOrNull)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile);
|
||||||
|
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
|
||||||
|
|
||||||
|
return new RecipientStore(objectMapper, file, recipientMergeHandler, recipients, storage.lastId);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
logger.debug("Creating new recipient store.");
|
logger.debug("Creating new recipient store.");
|
||||||
return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
|
return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
|
||||||
|
@ -60,7 +109,7 @@ public class RecipientStore {
|
||||||
final ObjectMapper objectMapper,
|
final ObjectMapper objectMapper,
|
||||||
final File file,
|
final File file,
|
||||||
final RecipientMergeHandler recipientMergeHandler,
|
final RecipientMergeHandler recipientMergeHandler,
|
||||||
final Map<RecipientId, SignalServiceAddress> recipients,
|
final Map<RecipientId, Recipient> recipients,
|
||||||
final long lastId
|
final long lastId
|
||||||
) {
|
) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
|
@ -71,6 +120,12 @@ public class RecipientStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) {
|
public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) {
|
||||||
|
synchronized (recipients) {
|
||||||
|
return getRecipient(recipientId).getAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recipient getRecipient(RecipientId recipientId) {
|
||||||
synchronized (recipients) {
|
synchronized (recipients) {
|
||||||
while (recipientsMerged.containsKey(recipientId)) {
|
while (recipientsMerged.containsKey(recipientId)) {
|
||||||
recipientId = recipientsMerged.get(recipientId);
|
recipientId = recipientsMerged.get(recipientId);
|
||||||
|
@ -92,11 +147,11 @@ public class RecipientStore {
|
||||||
return resolveRecipient(new SignalServiceAddress(null, number), false);
|
return resolveRecipient(new SignalServiceAddress(null, number), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientId resolveRecipient(SignalServiceAddress address) {
|
public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
|
||||||
return resolveRecipient(address, true);
|
return resolveRecipient(address, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RecipientId> resolveRecipients(List<SignalServiceAddress> addresses) {
|
public List<RecipientId> resolveRecipientsTrusted(List<SignalServiceAddress> addresses) {
|
||||||
final List<RecipientId> recipientIds;
|
final List<RecipientId> recipientIds;
|
||||||
final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
|
final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
|
||||||
synchronized (recipients) {
|
synchronized (recipients) {
|
||||||
|
@ -114,10 +169,84 @@ public class RecipientStore {
|
||||||
return recipientIds;
|
return recipientIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientId resolveRecipientUntrusted(SignalServiceAddress address) {
|
public RecipientId resolveRecipient(SignalServiceAddress address) {
|
||||||
return resolveRecipient(address, false);
|
return resolveRecipient(address, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeContact(final RecipientId recipientId, final Contact contact) {
|
||||||
|
synchronized (recipients) {
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Contact getContact(final RecipientId recipientId) {
|
||||||
|
final var recipient = getRecipient(recipientId);
|
||||||
|
return recipient == null ? null : recipient.getContact();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||||
|
return recipients.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> e.getValue().getContact() != null)
|
||||||
|
.map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Profile getProfile(final RecipientId recipientId) {
|
||||||
|
final var recipient = getRecipient(recipientId);
|
||||||
|
return recipient == null ? null : recipient.getProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProfileKey getProfileKey(final RecipientId recipientId) {
|
||||||
|
final var recipient = getRecipient(recipientId);
|
||||||
|
return recipient == null ? null : recipient.getProfileKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
|
||||||
|
final var recipient = getRecipient(recipientId);
|
||||||
|
return recipient == null ? null : recipient.getProfileKeyCredential();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeProfile(final RecipientId recipientId, final Profile profile) {
|
||||||
|
synchronized (recipients) {
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
|
||||||
|
synchronized (recipients) {
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfileKey(profileKey).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeProfileKeyCredential(
|
||||||
|
final RecipientId recipientId, final ProfileKeyCredential profileKeyCredential
|
||||||
|
) {
|
||||||
|
synchronized (recipients) {
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
storeRecipientLocked(recipientId,
|
||||||
|
Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
synchronized (recipients) {
|
||||||
|
return recipients.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
|
* @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
|
||||||
* Has no effect, if the address contains only a number or a uuid.
|
* Has no effect, if the address contains only a number or a uuid.
|
||||||
|
@ -141,20 +270,20 @@ public class RecipientStore {
|
||||||
SignalServiceAddress address, boolean isHighTrust
|
SignalServiceAddress address, boolean isHighTrust
|
||||||
) {
|
) {
|
||||||
final var byNumber = !address.getNumber().isPresent()
|
final var byNumber = !address.getNumber().isPresent()
|
||||||
? Optional.<RecipientId>empty()
|
? Optional.<Recipient>empty()
|
||||||
: findByName(address.getNumber().get());
|
: findByNameLocked(address.getNumber().get());
|
||||||
final var byUuid = !address.getUuid().isPresent()
|
final var byUuid = !address.getUuid().isPresent()
|
||||||
? Optional.<RecipientId>empty()
|
? Optional.<Recipient>empty()
|
||||||
: findByUuid(address.getUuid().get());
|
: findByUuidLocked(address.getUuid().get());
|
||||||
|
|
||||||
if (byNumber.isEmpty() && byUuid.isEmpty()) {
|
if (byNumber.isEmpty() && byUuid.isEmpty()) {
|
||||||
logger.debug("Got new recipient, both uuid and number are unknown");
|
logger.debug("Got new recipient, both uuid and number are unknown");
|
||||||
|
|
||||||
if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) {
|
if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) {
|
||||||
return new Pair<>(addNewRecipient(address), Optional.empty());
|
return new Pair<>(addNewRecipientLocked(address), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Pair<>(addNewRecipient(new SignalServiceAddress(address.getUuid().get(), null)),
|
return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)),
|
||||||
Optional.empty());
|
Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,79 +291,138 @@ public class RecipientStore {
|
||||||
|| !address.getUuid().isPresent()
|
|| !address.getUuid().isPresent()
|
||||||
|| !address.getNumber().isPresent()
|
|| !address.getNumber().isPresent()
|
||||||
|| byNumber.equals(byUuid)) {
|
|| byNumber.equals(byUuid)) {
|
||||||
return new Pair<>(byUuid.orElseGet(byNumber::get), Optional.empty());
|
return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (byNumber.isEmpty()) {
|
if (byNumber.isEmpty()) {
|
||||||
logger.debug("Got recipient existing with uuid, updating with high trust number");
|
logger.debug("Got recipient existing with uuid, updating with high trust number");
|
||||||
recipients.put(byUuid.get(), address);
|
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
|
||||||
save();
|
return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
|
||||||
return new Pair<>(byUuid.get(), Optional.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (byUuid.isEmpty()) {
|
if (byUuid.isEmpty()) {
|
||||||
logger.debug("Got recipient existing with number, updating with high trust uuid");
|
logger.debug("Got recipient existing with number, updating with high trust uuid");
|
||||||
recipients.put(byNumber.get(), address);
|
updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
|
||||||
save();
|
return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
|
||||||
return new Pair<>(byNumber.get(), Optional.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var byNumberAddress = recipients.get(byNumber.get());
|
if (byNumber.get().getAddress().getUuid().isPresent()) {
|
||||||
if (byNumberAddress.getUuid().isPresent()) {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
|
"Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
|
||||||
|
|
||||||
recipients.put(byNumber.get(), new SignalServiceAddress(byNumberAddress.getUuid().get(), null));
|
updateRecipientAddressLocked(byNumber.get().getRecipientId(),
|
||||||
recipients.put(byUuid.get(), address);
|
new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
|
||||||
save();
|
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
|
||||||
return new Pair<>(byUuid.get(), Optional.empty());
|
return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
|
logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
|
||||||
recipients.put(byUuid.get(), address);
|
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
|
||||||
recipients.remove(byNumber.get());
|
mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
|
||||||
save();
|
return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
|
||||||
return new Pair<>(byUuid.get(), byNumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecipientId addNewRecipient(final SignalServiceAddress serviceAddress) {
|
private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
|
||||||
final var nextRecipientId = nextId();
|
final var nextRecipientId = nextIdLocked();
|
||||||
recipients.put(nextRecipientId, serviceAddress);
|
storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
|
||||||
save();
|
|
||||||
return nextRecipientId;
|
return nextRecipientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<RecipientId> findByName(final String number) {
|
private void updateRecipientAddressLocked(
|
||||||
|
final RecipientId recipientId, final SignalServiceAddress address
|
||||||
|
) {
|
||||||
|
final var nextRecipientId = nextIdLocked();
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
storeRecipientLocked(nextRecipientId, Recipient.newBuilder(recipient).withAddress(address).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeRecipientLocked(
|
||||||
|
final RecipientId recipientId, final Recipient recipient
|
||||||
|
) {
|
||||||
|
recipients.put(recipientId, recipient);
|
||||||
|
saveLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
|
||||||
|
final var recipient = recipients.get(recipientId);
|
||||||
|
final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
|
||||||
|
recipients.put(recipientId,
|
||||||
|
new Recipient(recipientId,
|
||||||
|
recipient.getAddress(),
|
||||||
|
recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
|
||||||
|
recipient.getProfileKey() != null
|
||||||
|
? recipient.getProfileKey()
|
||||||
|
: toBeMergedRecipient.getProfileKey(),
|
||||||
|
recipient.getProfileKeyCredential() != null
|
||||||
|
? recipient.getProfileKeyCredential()
|
||||||
|
: toBeMergedRecipient.getProfileKeyCredential(),
|
||||||
|
recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
|
||||||
|
recipients.remove(toBeMergedRecipientId);
|
||||||
|
saveLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Recipient> findByNameLocked(final String number) {
|
||||||
return recipients.entrySet()
|
return recipients.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(entry -> entry.getValue().getNumber().isPresent() && number.equals(entry.getValue()
|
.filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
|
||||||
|
.getAddress()
|
||||||
.getNumber()
|
.getNumber()
|
||||||
.get()))
|
.get()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(Map.Entry::getKey);
|
.map(Map.Entry::getValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<RecipientId> findByUuid(final UUID uuid) {
|
private Optional<Recipient> findByUuidLocked(final UUID uuid) {
|
||||||
return recipients.entrySet()
|
return recipients.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(entry -> entry.getValue().getUuid().isPresent() && uuid.equals(entry.getValue()
|
.filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
|
||||||
|
.getAddress()
|
||||||
.getUuid()
|
.getUuid()
|
||||||
.get()))
|
.get()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(Map.Entry::getKey);
|
.map(Map.Entry::getValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecipientId nextId() {
|
private RecipientId nextIdLocked() {
|
||||||
return new RecipientId(++this.lastId);
|
return new RecipientId(++this.lastId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void save() {
|
private void saveLocked() {
|
||||||
var storage = new Storage(recipients.entrySet()
|
final var base64 = Base64.getEncoder();
|
||||||
.stream()
|
var storage = new Storage(recipients.entrySet().stream().map(pair -> {
|
||||||
.map(pair -> new Storage.Recipient(pair.getKey().getId(),
|
final var recipient = pair.getValue();
|
||||||
pair.getValue().getNumber().orNull(),
|
final var contact = recipient.getContact() == null
|
||||||
pair.getValue().getUuid().transform(UUID::toString).orNull()))
|
? null
|
||||||
.collect(Collectors.toList()), lastId);
|
: new Storage.Recipient.Contact(recipient.getContact().getName(),
|
||||||
|
recipient.getContact().getColor(),
|
||||||
|
recipient.getContact().getMessageExpirationTime(),
|
||||||
|
recipient.getContact().isBlocked(),
|
||||||
|
recipient.getContact().isArchived());
|
||||||
|
final var profile = recipient.getProfile() == null
|
||||||
|
? null
|
||||||
|
: new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
|
||||||
|
recipient.getProfile().getGivenName(),
|
||||||
|
recipient.getProfile().getFamilyName(),
|
||||||
|
recipient.getProfile().getAbout(),
|
||||||
|
recipient.getProfile().getAboutEmoji(),
|
||||||
|
recipient.getProfile().getUnidentifiedAccessMode().name(),
|
||||||
|
recipient.getProfile()
|
||||||
|
.getCapabilities()
|
||||||
|
.stream()
|
||||||
|
.map(Enum::name)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
|
return new Storage.Recipient(pair.getKey().getId(),
|
||||||
|
recipient.getAddress().getNumber().orNull(),
|
||||||
|
recipient.getAddress().getUuid().transform(UUID::toString).orNull(),
|
||||||
|
recipient.getProfileKey() == null
|
||||||
|
? null
|
||||||
|
: base64.encodeToString(recipient.getProfileKey().serialize()),
|
||||||
|
recipient.getProfileKeyCredential() == null
|
||||||
|
? null
|
||||||
|
: base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
|
||||||
|
contact,
|
||||||
|
profile);
|
||||||
|
}).collect(Collectors.toList()), lastId);
|
||||||
|
|
||||||
// Write to memory first to prevent corrupting the file in case of serialization errors
|
// Write to memory first to prevent corrupting the file in case of serialization errors
|
||||||
try (var inMemoryOutput = new ByteArrayOutputStream()) {
|
try (var inMemoryOutput = new ByteArrayOutputStream()) {
|
||||||
|
@ -249,17 +437,11 @@ public class RecipientStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
synchronized (recipients) {
|
|
||||||
return recipients.isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Storage {
|
private static class Storage {
|
||||||
|
|
||||||
private List<Recipient> recipients;
|
public List<Recipient> recipients;
|
||||||
|
|
||||||
private long lastId;
|
public long lastId;
|
||||||
|
|
||||||
// For deserialization
|
// For deserialization
|
||||||
private Storage() {
|
private Storage() {
|
||||||
|
@ -270,40 +452,96 @@ public class RecipientStore {
|
||||||
this.lastId = lastId;
|
this.lastId = lastId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipient> getRecipients() {
|
private static class Recipient {
|
||||||
return recipients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLastId() {
|
public long id;
|
||||||
return lastId;
|
public String number;
|
||||||
}
|
public String uuid;
|
||||||
|
public String profileKey;
|
||||||
public static class Recipient {
|
public String profileKeyCredential;
|
||||||
|
public Contact contact;
|
||||||
private long id;
|
public Profile profile;
|
||||||
private String name;
|
|
||||||
private String uuid;
|
|
||||||
|
|
||||||
// For deserialization
|
// For deserialization
|
||||||
private Recipient() {
|
private Recipient() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recipient(final long id, final String name, final String uuid) {
|
public Recipient(
|
||||||
|
final long id,
|
||||||
|
final String number,
|
||||||
|
final String uuid,
|
||||||
|
final String profileKey,
|
||||||
|
final String profileKeyCredential,
|
||||||
|
final Contact contact,
|
||||||
|
final Profile profile
|
||||||
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.number = number;
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
|
this.profileKey = profileKey;
|
||||||
|
this.profileKeyCredential = profileKeyCredential;
|
||||||
|
this.contact = contact;
|
||||||
|
this.profile = profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getId() {
|
private static class Contact {
|
||||||
return id;
|
|
||||||
|
public String name;
|
||||||
|
public String color;
|
||||||
|
public int messageExpirationTime;
|
||||||
|
public boolean blocked;
|
||||||
|
public boolean archived;
|
||||||
|
|
||||||
|
// For deserialization
|
||||||
|
public Contact() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Contact(
|
||||||
|
final String name,
|
||||||
|
final String color,
|
||||||
|
final int messageExpirationTime,
|
||||||
|
final boolean blocked,
|
||||||
|
final boolean archived
|
||||||
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.color = color;
|
||||||
|
this.messageExpirationTime = messageExpirationTime;
|
||||||
|
this.blocked = blocked;
|
||||||
|
this.archived = archived;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
private static class Profile {
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUuid() {
|
public long lastUpdateTimestamp;
|
||||||
return uuid;
|
public String givenName;
|
||||||
|
public String familyName;
|
||||||
|
public String about;
|
||||||
|
public String aboutEmoji;
|
||||||
|
public String unidentifiedAccessMode;
|
||||||
|
public Set<String> capabilities;
|
||||||
|
|
||||||
|
// For deserialization
|
||||||
|
private Profile() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Profile(
|
||||||
|
final long lastUpdateTimestamp,
|
||||||
|
final String givenName,
|
||||||
|
final String familyName,
|
||||||
|
final String about,
|
||||||
|
final String aboutEmoji,
|
||||||
|
final String unidentifiedAccessMode,
|
||||||
|
final Set<String> capabilities
|
||||||
|
) {
|
||||||
|
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
||||||
|
this.givenName = givenName;
|
||||||
|
this.familyName = familyName;
|
||||||
|
this.about = about;
|
||||||
|
this.aboutEmoji = aboutEmoji;
|
||||||
|
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package org.asamk.signal.manager.util;
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
import org.asamk.signal.manager.storage.profiles.SignalProfile;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
public class ProfileUtils {
|
public class ProfileUtils {
|
||||||
|
|
||||||
public static SignalProfile decryptProfile(
|
public static Profile decryptProfile(
|
||||||
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
||||||
) {
|
) {
|
||||||
var profileCipher = new ProfileCipher(profileKey);
|
var profileCipher = new ProfileCipher(profileKey);
|
||||||
|
@ -28,13 +31,28 @@ public class ProfileUtils {
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
unidentifiedAccess = null;
|
unidentifiedAccess = null;
|
||||||
}
|
}
|
||||||
return new SignalProfile(encryptedProfile.getIdentityKey(),
|
final var nameParts = splitName(name);
|
||||||
name,
|
final var capabilities = new HashSet<Profile.Capability>();
|
||||||
|
if (encryptedProfile.getCapabilities().isGv1Migration()) {
|
||||||
|
capabilities.add(Profile.Capability.gv1Migration);
|
||||||
|
}
|
||||||
|
if (encryptedProfile.getCapabilities().isGv2()) {
|
||||||
|
capabilities.add(Profile.Capability.gv2);
|
||||||
|
}
|
||||||
|
if (encryptedProfile.getCapabilities().isStorage()) {
|
||||||
|
capabilities.add(Profile.Capability.storage);
|
||||||
|
}
|
||||||
|
return new Profile(new Date().getTime(),
|
||||||
|
nameParts.first(),
|
||||||
|
nameParts.second(),
|
||||||
about,
|
about,
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
unidentifiedAccess,
|
encryptedProfile.isUnrestrictedUnidentifiedAccess()
|
||||||
encryptedProfile.isUnrestrictedUnidentifiedAccess(),
|
? Profile.UnidentifiedAccessMode.UNRESTRICTED
|
||||||
encryptedProfile.getCapabilities());
|
: unidentifiedAccess != null
|
||||||
|
? Profile.UnidentifiedAccessMode.ENABLED
|
||||||
|
: Profile.UnidentifiedAccessMode.DISABLED,
|
||||||
|
capabilities);
|
||||||
} catch (InvalidCiphertextException e) {
|
} catch (InvalidCiphertextException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -51,4 +69,17 @@ public class ProfileUtils {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Pair<String, String> splitName(String name) {
|
||||||
|
String[] parts = name.split("\0");
|
||||||
|
|
||||||
|
switch (parts.length) {
|
||||||
|
case 0:
|
||||||
|
return new Pair<>(null, null);
|
||||||
|
case 1:
|
||||||
|
return new Pair<>(parts[0], null);
|
||||||
|
default:
|
||||||
|
return new Pair<>(parts[0], parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ public interface Signal extends DBusInterface {
|
||||||
|
|
||||||
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure;
|
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure;
|
||||||
|
|
||||||
boolean isContactBlocked(final String number);
|
boolean isContactBlocked(final String number) throws Error.InvalidNumber;
|
||||||
|
|
||||||
boolean isGroupBlocked(final byte[] groupId);
|
boolean isGroupBlocked(final byte[] groupId);
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
@ -667,7 +668,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
||||||
|
|
||||||
private String formatContact(SignalServiceAddress address) {
|
private String formatContact(SignalServiceAddress address) {
|
||||||
final var number = address.getLegacyIdentifier();
|
final var number = address.getLegacyIdentifier();
|
||||||
var name = m.getContactOrProfileName(number);
|
String name = null;
|
||||||
|
try {
|
||||||
|
name = m.getContactOrProfileName(number);
|
||||||
|
} catch (InvalidNumberException ignored) {
|
||||||
|
}
|
||||||
if (name == null || name.isEmpty()) {
|
if (name == null || name.isEmpty()) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -18,7 +18,10 @@ public class ListContactsCommand implements LocalCommand {
|
||||||
|
|
||||||
var contacts = m.getContacts();
|
var contacts = m.getContacts();
|
||||||
for (var c : contacts) {
|
for (var c : contacts) {
|
||||||
writer.println("Number: {} Name: {} Blocked: {}", c.number, c.name, c.blocked);
|
writer.println("Number: {} Name: {} Blocked: {}",
|
||||||
|
m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier(),
|
||||||
|
c.second().getName(),
|
||||||
|
c.second().isBlocked());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||||
import org.asamk.signal.util.ErrorUtils;
|
import org.asamk.signal.util.ErrorUtils;
|
||||||
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
import org.freedesktop.dbus.exceptions.DBusExecutionException;
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
|
@ -248,7 +249,7 @@ public class DbusSignalImpl implements Signal {
|
||||||
public String getContactName(final String number) {
|
public String getContactName(final String number) {
|
||||||
try {
|
try {
|
||||||
return m.getContactOrProfileName(number);
|
return m.getContactOrProfileName(number);
|
||||||
} catch (Exception e) {
|
} catch (InvalidNumberException e) {
|
||||||
throw new Error.InvalidNumber(e.getMessage());
|
throw new Error.InvalidNumber(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -383,11 +384,10 @@ public class DbusSignalImpl implements Signal {
|
||||||
// all numbers the system knows
|
// all numbers the system knows
|
||||||
@Override
|
@Override
|
||||||
public List<String> listNumbers() {
|
public List<String> listNumbers() {
|
||||||
return Stream.concat(m.getIdentities()
|
return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
|
||||||
.stream()
|
m.getContacts().stream().map(Pair::first))
|
||||||
.map(IdentityInfo::getRecipientId)
|
|
||||||
.map(m::resolveSignalServiceAddress)
|
.map(m::resolveSignalServiceAddress)
|
||||||
.map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number))
|
.map(a -> a.getNumber().orNull())
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
@ -399,8 +399,8 @@ public class DbusSignalImpl implements Signal {
|
||||||
var numbers = new ArrayList<String>();
|
var numbers = new ArrayList<String>();
|
||||||
var contacts = m.getContacts();
|
var contacts = m.getContacts();
|
||||||
for (var c : contacts) {
|
for (var c : contacts) {
|
||||||
if (c.name != null && c.name.equals(name)) {
|
if (name.equals(c.second().getName())) {
|
||||||
numbers.add(c.number);
|
numbers.add(m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Try profiles if no contact name was found
|
// Try profiles if no contact name was found
|
||||||
|
@ -449,13 +449,11 @@ public class DbusSignalImpl implements Signal {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isContactBlocked(final String number) {
|
public boolean isContactBlocked(final String number) {
|
||||||
var contacts = m.getContacts();
|
try {
|
||||||
for (var c : contacts) {
|
return m.isContactBlocked(number);
|
||||||
if (c.number.equals(number)) {
|
} catch (InvalidNumberException e) {
|
||||||
return c.blocked;
|
throw new Error.InvalidNumber(e.getMessage());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue