import com.fasterxml.jackson.databind.ObjectMapper;
-import org.asamk.Signal;
-import org.asamk.signal.AttachmentInvalidException;
-import org.asamk.signal.GroupNotFoundException;
-import org.asamk.signal.NotAGroupMemberException;
-import org.asamk.signal.StickerPackInvalidException;
-import org.asamk.signal.TrustLevel;
import org.asamk.signal.storage.SignalAccount;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.asamk.signal.storage.profiles.SignalProfileEntry;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.InvalidInputException;
-import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
+import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
-public class Manager implements Signal {
+import static org.asamk.signal.manager.ServiceConfig.capabilities;
+
+public class Manager implements Closeable {
private final SleepTimer timer = new UptimeSleepTimer();
private final SignalServiceConfiguration serviceConfiguration;
private SignalServiceAccountManager accountManager;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
+ private boolean discoverableByPhoneNumber = true;
public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
this.account = account;
}
private SignalServiceAccountManager createSignalServiceAccountManager() {
- return new SignalServiceAccountManager(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), userAgent, timer);
+ GroupsV2Operations groupsV2Operations;
+ try {
+ groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
+ } catch (Throwable ignored) {
+ groupsV2Operations = null;
+ }
+ return new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()),
+ userAgent,
+ groupsV2Operations,
+ timer);
}
private IdentityKeyPair getIdentityKeyPair() {
Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent);
m.migrateLegacyConfigs();
- m.checkAccountState();
return m;
}
}
}
- private void checkAccountState() throws IOException {
+ public void checkAccountState() throws IOException {
if (account.isRegistered()) {
if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
refreshPreKeys();
}
public void updateAccountAttributes() throws IOException {
- accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities);
- }
-
- public void setProfileName(String name) throws IOException {
- accountManager.setProfileName(account.getProfileKey(), name);
- }
-
- public void setProfileAvatar(File avatar) throws IOException {
- final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar);
- accountManager.setProfileAvatar(account.getProfileKey(), streamDetails);
- streamDetails.getStream().close();
+ accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
}
- public void removeProfileAvatar() throws IOException {
- accountManager.setProfileAvatar(account.getProfileKey(), null);
+ public void setProfile(String name, File avatar) throws IOException {
+ try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) {
+ accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails);
+ }
}
public void unregister() throws IOException {
verificationCode = verificationCode.replace("-", "");
account.setSignalingKey(KeyUtils.createSignalingKey());
// TODO make unrestricted unidentified access configurable
- VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities);
+ VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
UUID uuid = UuidUtil.parseOrNull(response.getUuid());
// TODO response.isStorageCapable()
// TODO implement ZkGroup support
final ClientZkProfileOperations clientZkProfileOperations = null;
final boolean attachmentsV3 = false;
+ final ExecutorService executor = null;
return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
- account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations);
+ account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor);
}
- private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
+ private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
: messagePipe;
if (pipe != null) {
try {
- return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
- } catch (IOException ignored) {
+ return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+ } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) {
}
}
SignalServiceMessageReceiver receiver = getMessageReceiver();
try {
- return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
- } catch (VerificationFailedException e) {
- throw new AssertionError(e);
+ return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new IOException("Failed to retrieve profile", e);
+ }
+ }
+
+ private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address);
+ long now = new Date().getTime();
+ // Profiles are cache for 24h before retrieving them again
+ if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
+ SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey);
+ account.getProfileStore().updateProfile(address, profileKey, now, profile);
+ return profile;
+ }
+ return profileEntry.getProfile();
+ }
+
+ private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
+ final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess);
+
+ File avatarFile = null;
+ try {
+ avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+ } catch (Throwable e) {
+ System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage());
+ }
+
+ ProfileCipher profileCipher = new ProfileCipher(profileKey);
+ try {
+ return new SignalProfile(
+ encryptedProfile.getIdentityKey(),
+ encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+ avatarFile,
+ encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+ encryptedProfile.isUnrestrictedUnidentifiedAccess(),
+ encryptedProfile.getCapabilities());
+ } catch (InvalidCiphertextException e) {
+ return null;
}
}
return account.getGroupStore().getGroups();
}
- @Override
public long sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId)
- throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor,
long targetSentTimestamp, byte[] groupId)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
+ throws IOException, EncapsulatedExceptions, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
+ public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, NotAGroupMemberException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
.withId(groupId)
.build();
sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+ private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g;
if (groupId == null) {
// Create new group
for (ContactTokenDetails contact : contacts) {
newE164Members.remove(contact.getNumber());
}
- System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
- System.err.println("Aborting…");
- System.exit(1);
+ throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
}
g.addMembers(members);
return g.groupId;
}
- private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
+ void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
if (groupId == null) {
return;
}
sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
- private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
+ private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) throws AttachmentInvalidException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
.withId(g.groupId)
.withName(g.name)
.withExpiration(g.messageExpirationTime);
}
- private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
+ void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
if (groupId == null) {
return;
}
sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
}
- private void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
+ void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
Collections.singletonList(messageId),
System.currentTimeMillis());
getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage);
}
- @Override
- public long sendMessage(String message, List<String> attachments, String recipient)
- throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException {
- List<String> recipients = new ArrayList<>(1);
- recipients.add(recipient);
- return sendMessage(message, attachments, recipients);
- }
-
- @Override
public long sendMessage(String messageText, List<String> attachments,
List<String> recipients)
throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
public void sendMessageReaction(String emoji, boolean remove, String targetAuthor,
long targetSentTimestamp, List<String> recipients)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
+ throws IOException, EncapsulatedExceptions, InvalidNumberException {
SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
}
- @Override
public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException {
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asEndSessionMessage();
}
}
- @Override
public String getContactName(String number) throws InvalidNumberException {
ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number));
if (contact == null) {
}
}
- @Override
public void setContactName(String number, String name) throws InvalidNumberException {
final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
contact = new ContactInfo(address);
- System.err.println("Add contact " + contact.number + " named " + name);
- } else {
- System.err.println("Updating contact " + contact.number + " name " + contact.name + " -> " + name);
}
contact.name = name;
account.getContactStore().updateContact(contact);
account.save();
}
- @Override
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked);
}
ContactInfo contact = account.getContactStore().getContact(address);
if (contact == null) {
contact = new ContactInfo(address);
- System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + address.getNumber().orNull());
- } else {
- System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + address.getNumber().orNull());
}
contact.blocked = blocked;
account.getContactStore().updateContact(contact);
account.save();
}
- @Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException {
GroupInfo group = getGroup(groupId);
if (group == null) {
throw new GroupNotFoundException(groupId);
- } else {
- System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId));
- group.blocked = blocked;
- account.getGroupStore().updateGroup(group);
- account.save();
}
- }
- @Override
- public List<byte[]> getGroupIds() {
- List<GroupInfo> groups = getGroups();
- List<byte[]> ids = new ArrayList<>(groups.size());
- for (GroupInfo group : groups) {
- ids.add(group.groupId);
- }
- return ids;
- }
-
- @Override
- public String getGroupName(byte[] groupId) {
- GroupInfo group = getGroup(groupId);
- if (group == null) {
- return "";
- } else {
- return group.name;
- }
- }
-
- @Override
- public List<String> getGroupMembers(byte[] groupId) {
- GroupInfo group = getGroup(groupId);
- if (group == null) {
- return Collections.emptyList();
- } else {
- return new ArrayList<>(group.getMembersE164());
- }
+ group.blocked = blocked;
+ account.getGroupStore().updateGroup(group);
+ account.save();
}
- @Override
- public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException {
+ public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
if (groupId.length == 0) {
groupId = null;
}
if (name.isEmpty()) {
name = null;
}
- if (members.size() == 0) {
+ if (members.isEmpty()) {
members = null;
}
if (avatar.isEmpty()) {
/**
* Change the expiration timer for a contact
*/
- public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) {
- ContactInfo c = account.getContactStore().getContact(address);
- c.messageExpirationTime = messageExpirationTimer;
- account.getContactStore().updateContact(c);
+ public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException {
+ ContactInfo contact = account.getContactStore().getContact(address);
+ contact.messageExpirationTime = messageExpirationTimer;
+ account.getContactStore().updateContact(contact);
+ sendExpirationTimerUpdate(address);
+ account.save();
+ }
+
+ private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .asExpirationUpdate();
+ sendMessage(messageBuilder, Collections.singleton(address));
+ }
+
+ /**
+ * Change the expiration timer for a contact
+ */
+ public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException {
+ SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
+ setExpirationTimer(address, messageExpirationTimer);
}
/**
throw new StickerPackInvalidException("Could not find find " + sticker.file);
}
- StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""));
+ String contentType = Utils.getFileMimeType(new File(sticker.file), null);
+ StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType);
stickers.add(stickerInfo);
}
throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
}
- cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""));
+ String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
+ cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType);
}
return new SignalServiceStickerManifestUpload(
private byte[] getSenderCertificate() {
// TODO support UUID capable sender certificates
- // byte[] certificate = accountManager.getSenderCertificate();
+ // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
byte[] certificate;
try {
- certificate = accountManager.getSenderCertificateLegacy();
+ certificate = accountManager.getSenderCertificate();
} catch (IOException e) {
System.err.println("Failed to get sender certificate: " + e);
return null;
return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
}
- private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException {
- ProfileCipher profileCipher = new ProfileCipher(profileKey);
- try {
- return new SignalProfile(
- encryptedProfile.getIdentityKey(),
- encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
- encryptedProfile.getAvatar(),
- encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
- encryptedProfile.isUnrestrictedUnidentifiedAccess()
- );
- } catch (InvalidCiphertextException e) {
- return null;
- }
- }
-
private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
ContactInfo contact = account.getContactStore().getContact(recipient);
if (contact == null || contact.profileKey == null) {
}
SignalProfile targetProfile;
try {
- targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey);
+ targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey);
} catch (IOException e) {
System.err.println("Failed to get recipient profile: " + e);
return null;
}
SignalServiceDataMessage message = null;
try {
- SignalServiceMessageSender messageSender = getMessageSender();
-
message = messageBuilder.build();
if (message.getGroupContext().isPresent()) {
try {
+ SignalServiceMessageSender messageSender = getMessageSender();
final boolean isRecipientUpdate = false;
List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
for (SendMessageResult r : result) {
account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
return Collections.emptyList();
}
- } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) {
- SignalServiceAddress recipient = account.getSelfAddress();
- final Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
- SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
- message.getTimestamp(),
- message,
- message.getExpiresInSeconds(),
- Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
- false);
- SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
-
- List<SendMessageResult> results = new ArrayList<>(recipients.size());
- try {
- messageSender.sendMessage(syncMessage, unidentifiedAccess);
- } catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey()));
- }
- return results;
} else {
// Send to all individually, so sync messages are sent correctly
List<SendMessageResult> results = new ArrayList<>(recipients.size());
messageBuilder.withProfileKey(null);
}
message = messageBuilder.build();
- try {
- SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message);
- results.add(result);
- } catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- results.add(SendMessageResult.identityFailure(address, e.getIdentityKey()));
+ if (address.matches(account.getSelfAddress())) {
+ results.add(sendSelfMessage(message));
+ } else {
+ results.add(sendMessage(address, message));
}
}
return results;
}
}
+ private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ SignalServiceAddress recipient = account.getSelfAddress();
+
+ final Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
+ SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
+ message.getTimestamp(),
+ message,
+ message.getExpiresInSeconds(),
+ Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
+ false);
+ SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
+
+ try {
+ messageSender.sendMessage(syncMessage, unidentifiedAccess);
+ return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false);
+ } catch (UntrustedIdentityException e) {
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
+ }
+ }
+
+ private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException {
+ SignalServiceMessageSender messageSender = getMessageSender();
+
+ try {
+ return messageSender.sendMessage(address, getAccessFor(address), message);
+ } catch (UntrustedIdentityException e) {
+ account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ return SendMessageResult.identityFailure(address, e.getIdentityKey());
+ }
+ }
+
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
try {
account.getSignalProtocolStore().deleteAllSessions(source);
}
- private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ private List<HandleAction> handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ List<HandleAction> actions = new ArrayList<>();
if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
account.getGroupStore().updateGroup(group);
break;
case DELIVER:
- if (group == null) {
- try {
- sendGroupInfoRequest(groupInfo.getGroupId(), source);
- } catch (IOException | EncapsulatedExceptions e) {
- e.printStackTrace();
- }
+ if (group == null && !isSync) {
+ actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId()));
}
break;
case QUIT:
}
break;
case REQUEST_INFO:
- if (group != null) {
- try {
- sendUpdateGroupMessage(groupInfo.getGroupId(), source);
- } catch (IOException | EncapsulatedExceptions e) {
- e.printStackTrace();
- } catch (NotAGroupMemberException e) {
- // We have left this group, so don't send a group update message
- }
+ if (group != null && !isSync) {
+ actions.add(new SendGroupUpdateAction(source, group.groupId));
}
break;
}
}
}
}
+ return actions;
}
private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
} catch (Exception e) {
return;
}
- handleMessage(envelope, content, ignoreAttachments);
+ List<HandleAction> actions = handleMessage(envelope, content, ignoreAttachments);
+ for (HandleAction action : actions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
}
account.save();
handler.handleMessage(envelope, content, null);
retryFailedReceivedMessages(handler, ignoreAttachments);
final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
- try {
- if (messagePipe == null) {
- messagePipe = messageReceiver.createMessagePipe();
- }
+ Set<HandleAction> queuedActions = null;
- while (true) {
- SignalServiceEnvelope envelope;
- SignalServiceContent content = null;
- Exception exception = null;
- final long now = new Date().getTime();
- try {
- envelope = messagePipe.read(timeout, unit, envelope1 -> {
- // store message on disk, before acknowledging receipt to the server
- try {
- String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
- File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
- Utils.storeEnvelope(envelope1, cacheFile);
- } catch (IOException e) {
- System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
- }
- });
- } catch (TimeoutException e) {
- if (returnOnTimeout)
- return;
- continue;
- } catch (InvalidVersionException e) {
- System.err.println("Ignoring error: " + e.getMessage());
- continue;
- }
- if (!envelope.isReceipt()) {
+ if (messagePipe == null) {
+ messagePipe = messageReceiver.createMessagePipe();
+ }
+
+ boolean hasCaughtUpWithOldMessages = false;
+
+ while (true) {
+ SignalServiceEnvelope envelope;
+ SignalServiceContent content = null;
+ Exception exception = null;
+ final long now = new Date().getTime();
+ try {
+ Optional<SignalServiceEnvelope> result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
+ // store message on disk, before acknowledging receipt to the server
try {
- content = decryptMessage(envelope);
- } catch (Exception e) {
- exception = e;
+ String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
+ File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
+ Utils.storeEnvelope(envelope1, cacheFile);
+ } catch (IOException e) {
+ System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
+ }
+ });
+ if (result.isPresent()) {
+ envelope = result.get();
+ } else {
+ // Received indicator that server queue is empty
+ hasCaughtUpWithOldMessages = true;
+
+ if (queuedActions != null) {
+ for (HandleAction action : queuedActions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+ queuedActions.clear();
+ queuedActions = null;
}
- handleMessage(envelope, content, ignoreAttachments);
+
+ // Continue to wait another timeout for new messages
+ continue;
}
- account.save();
- if (!isMessageBlocked(envelope, content)) {
- handler.handleMessage(envelope, content, exception);
+ } catch (TimeoutException e) {
+ if (returnOnTimeout)
+ return;
+ continue;
+ } catch (InvalidVersionException e) {
+ System.err.println("Ignoring error: " + e.getMessage());
+ continue;
+ }
+ if (envelope.hasSource()) {
+ // Store uuid if we don't have it already
+ SignalServiceAddress source = envelope.getSourceAddress();
+ resolveSignalServiceAddress(source);
+ }
+ if (!envelope.isReceipt()) {
+ try {
+ content = decryptMessage(envelope);
+ } catch (Exception e) {
+ exception = e;
}
- if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
- File cacheFile = null;
- try {
- cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp());
- Files.delete(cacheFile.toPath());
- // Try to delete directory if empty
- new File(getMessageCachePath()).delete();
- } catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ List<HandleAction> actions = handleMessage(envelope, content, ignoreAttachments);
+ if (hasCaughtUpWithOldMessages) {
+ for (HandleAction action : actions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ if (queuedActions == null) {
+ queuedActions = new HashSet<>();
}
+ queuedActions.addAll(actions);
}
}
- } finally {
- if (messagePipe != null) {
- messagePipe.shutdown();
- messagePipe = null;
+ account.save();
+ if (!isMessageBlocked(envelope, content)) {
+ handler.handleMessage(envelope, content, exception);
+ }
+ if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
+ File cacheFile = null;
+ try {
+ String source = envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "";
+ cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
+ Files.delete(cacheFile.toPath());
+ // Try to delete directory if empty
+ new File(getMessageCachePath()).delete();
+ } catch (IOException e) {
+ System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ }
}
}
}
return false;
}
- private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ private List<HandleAction> handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ List<HandleAction> actions = new ArrayList<>();
if (content != null) {
SignalServiceAddress sender;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
} else {
sender = content.getSender();
}
+ // Store uuid if we don't have it already
+ resolveSignalServiceAddress(sender);
+
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
if (content.isNeedsReceipt()) {
- try {
- sendReceipt(sender, message.getTimestamp());
- } catch (IOException | UntrustedIdentityException | IllegalArgumentException e) {
- e.printStackTrace();
- }
+ actions.add(new SendReceiptAction(sender, message.getTimestamp()));
}
- handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments);
+ actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments));
}
if (content.getSyncMessage().isPresent()) {
account.setMultiDevice(true);
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) {
SentTranscriptMessage message = syncMessage.getSent().get();
- handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments);
+ actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments));
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
if (rm.isContactsRequest()) {
- try {
- sendContacts();
- } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncContactsAction.create());
}
if (rm.isGroupsRequest()) {
- try {
- sendGroups();
- } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncGroupsAction.create());
}
if (rm.isBlockedListRequest()) {
- try {
- sendBlockedList();
- } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) {
- e.printStackTrace();
- }
+ actions.add(SendSyncBlockedListAction.create());
}
// TODO Handle rm.isConfigurationRequest();
}
}
}
}
+ return actions;
}
private File getContactAvatarFile(String number) {
}
}
+ private File getProfileAvatarFile(SignalServiceAddress address) {
+ return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier());
+ }
+
+ private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
+ SignalServiceMessageReceiver receiver = getMessageReceiver();
+ File outputFile = getProfileAvatarFile(address);
+
+ File tmpFile = IOUtils.createTempFile();
+ try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+ // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
+ IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
+ } finally {
+ try {
+ Files.delete(tmpFile.toPath());
+ } catch (IOException e) {
+ System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage());
+ }
+ }
+ return outputFile;
+ }
+
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(pathConfig.getAttachmentsPath(), attachmentId.toString());
}
File tmpFile = IOUtils.createTempFile();
try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) {
- try (OutputStream output = new FileOutputStream(outputFile)) {
- byte[] buffer = new byte[4096];
- int read;
-
- while ((read = input.read(buffer)) != -1) {
- output.write(buffer, 0, read);
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return null;
- }
+ IOUtils.copyStreamToFile(input, outputFile);
} finally {
try {
Files.delete(tmpFile.toPath());
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
- @Override
- public boolean isRemote() {
- return false;
- }
-
- @Override
- public String getObjectPath() {
- return null;
- }
-
- private void sendGroups() throws IOException, UntrustedIdentityException {
+ void sendGroups() throws IOException, UntrustedIdentityException {
File groupsFile = IOUtils.createTempFile();
try {
}
}
- private void sendBlockedList() throws IOException, UntrustedIdentityException {
+ void sendBlockedList() throws IOException, UntrustedIdentityException {
List<SignalServiceAddress> addresses = new ArrayList<>();
for (ContactInfo record : account.getContactStore().getContacts()) {
if (record.blocked) {
return account.getRecipientStore().resolveServiceAddress(address);
}
+ @Override
+ public void close() throws IOException {
+ if (messagePipe != null) {
+ messagePipe.shutdown();
+ messagePipe = null;
+ }
+
+ if (unidentifiedMessagePipe != null) {
+ unidentifiedMessagePipe.shutdown();
+ unidentifiedMessagePipe = null;
+ }
+
+ account.close();
+ }
+
public interface ReceiveMessageHandler {
void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);