import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
+import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
+import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
+import org.asamk.signal.manager.api.InvalidNumberException;
import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope;
+import org.asamk.signal.manager.api.MessageEnvelope.Sync.MessageRequestResponse;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus;
import org.asamk.signal.manager.api.UsernameLinkUrl;
+import org.asamk.signal.manager.api.UsernameStatus;
+import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.AccountFileUpdater;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.helper.RecipientHelper.RegisteredUser;
+import org.asamk.signal.manager.jobs.RefreshRecipientsJob;
import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.AttachmentStore;
import org.asamk.signal.manager.storage.AvatarStore;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils;
+import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.asamk.signal.manager.util.StickerUtils;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.SignalSessionLock;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
+import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
+import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
+import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
-import org.whispersystems.signalservice.api.util.InvalidNumberException;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
+import okio.Utf8;
+
+import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
+import static org.asamk.signal.manager.util.Utils.handleResponseException;
+import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
public class ManagerImpl implements Manager {
private final List<Runnable> closedListeners = new ArrayList<>();
private final List<Runnable> addressChangedListeners = new ArrayList<>();
private final CompositeDisposable disposable = new CompositeDisposable();
+ private final AtomicLong lastMessageTimestamp = new AtomicLong();
public ManagerImpl(
SignalAccount account,
) {
this.account = account;
- final var sessionLock = new SignalSessionLock() {
- private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
-
- @Override
- public Lock acquire() {
- LEGACY_LOCK.lock();
- return LEGACY_LOCK::unlock;
- }
- };
+ final var sessionLock = new ReentrantSignalSessionLock();
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
account.getCredentialsProvider(),
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
if (lastRecipientsRefresh == null
|| lastRecipientsRefresh < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) {
- try {
- context.getRecipientHelper().refreshUsers();
- } catch (Exception e) {
- logger.warn("Full CDSI recipients refresh failed, ignoring: {} ({})",
- e.getMessage(),
- e.getClass().getSimpleName());
- logger.debug("Full CDSI refresh failed", e);
- }
+ context.getJobExecutor().enqueueJob(new RefreshRecipientsJob());
context.getAccountHelper().checkWhoAmiI();
}
}
}
@Override
- public void updateAccountAttributes(String deviceName, Boolean unrestrictedUnidentifiedSender) throws IOException {
+ public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
+ final var registeredUsers = new HashMap<String, RecipientAddress>();
+ for (final var username : usernames) {
+ try {
+ final var recipientId = context.getRecipientHelper().resolveRecipientByUsernameOrLink(username, true);
+ final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
+ registeredUsers.put(username, address);
+ } catch (UnregisteredRecipientException e) {
+ // ignore
+ }
+ }
+
+ return usernames.stream().collect(Collectors.toMap(n -> n, username -> {
+ final var user = registeredUsers.get(username);
+ final var serviceId = user == null ? null : user.serviceId().orElse(null);
+ final var profile = serviceId == null
+ ? null
+ : context.getProfileHelper()
+ .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
+ return new UsernameStatus(username,
+ serviceId == null ? null : serviceId.getRawUuid(),
+ profile != null
+ && profile.getUnidentifiedAccessMode() == Profile.UnidentifiedAccessMode.UNRESTRICTED);
+ }));
+ }
+
+ @Override
+ public void updateAccountAttributes(
+ String deviceName,
+ Boolean unrestrictedUnidentifiedSender,
+ final Boolean discoverableByNumber,
+ final Boolean numberSharing
+ ) throws IOException {
if (deviceName != null) {
context.getAccountHelper().setDeviceName(deviceName);
}
if (unrestrictedUnidentifiedSender != null) {
account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedSender);
}
+ if (discoverableByNumber != null) {
+ account.getConfigurationStore().setPhoneNumberUnlisted(!discoverableByNumber);
+ }
+ if (numberSharing != null) {
+ account.getConfigurationStore()
+ .setPhoneNumberSharingMode(numberSharing
+ ? PhoneNumberSharingMode.EVERYBODY
+ : PhoneNumberSharingMode.NOBODY);
+ }
context.getAccountHelper().updateAccountAttributes();
context.getAccountHelper().checkWhoAmiI();
}
@Override
public void setUsername(final String username) throws IOException, InvalidUsernameException {
try {
- context.getAccountHelper().reserveUsername(username);
+ if (username.contains(".")) {
+ context.getAccountHelper().reserveExactUsername(username);
+ } else {
+ context.getAccountHelper().reserveUsernameFromNickname(username);
+ }
+ } catch (UsernameMalformedException e) {
+ throw new InvalidUsernameException("Username is malformed", e);
+ } catch (UsernameTakenException e) {
+ throw new InvalidUsernameException("Username is already registered", e);
} catch (BaseUsernameException e) {
throw new InvalidUsernameException(e.getMessage() + " (" + e.getClass().getSimpleName() + ")", e);
}
@Override
public void startChangeNumber(
- String newNumber, boolean voiceVerification, String captcha
- ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException {
+ String newNumber,
+ boolean voiceVerification,
+ String captcha
+ ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
@Override
public void finishChangeNumber(
- String newNumber, String verificationCode, String pin
- ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
+ String newNumber,
+ String verificationCode,
+ String pin
+ ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
}
@Override
- public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
- captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+ public void submitRateLimitRecaptchaChallenge(
+ String challenge,
+ String captcha
+ ) throws IOException, CaptchaRejectedException {
+ captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
- dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
+ try {
+ handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
+ } catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
+ throw new CaptchaRejectedException();
+ }
}
@Override
public List<Device> getLinkedDevices() throws IOException {
- var devices = dependencies.getAccountManager().getDevices();
+ var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> {
}
@Override
- public void removeLinkedDevices(int deviceId) throws IOException {
+ public void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException {
+ if (!account.isPrimaryDevice()) {
+ throw new NotPrimaryDeviceException();
+ }
context.getAccountHelper().removeLinkedDevices(deviceId);
}
@Override
- public void addDeviceLink(DeviceLinkUrl linkUrl) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException {
+ public void addDeviceLink(DeviceLinkUrl linkUrl) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
@Override
public List<Group> getGroups() {
- return account.getGroupStore().getGroups().stream().map(this::toGroup).toList();
+ return context.getGroupHelper().getGroups().stream().map(this::toGroup).toList();
}
private Group toGroup(final GroupInfo groupInfo) {
@Override
public SendGroupMessageResults quitGroup(
- GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
+ GroupId groupId,
+ Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
return context.getGroupHelper().quitGroup(groupId, newAdmins);
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
- String name, Set<RecipientIdentifier.Single> members, String avatarFile
+ String name,
+ Set<RecipientIdentifier.Single> members,
+ String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
return context.getGroupHelper()
.createGroup(name,
@Override
public SendGroupMessageResults updateGroup(
- final GroupId groupId, final UpdateGroup updateGroup
+ final GroupId groupId,
+ final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
return context.getGroupHelper()
.updateGroup(groupId,
return context.getGroupHelper().joinGroup(inviteLinkUrl);
}
+ private long getNextMessageTimestamp() {
+ while (true) {
+ final var last = lastMessageTimestamp.get();
+ final var timestamp = System.currentTimeMillis();
+ if (last == timestamp) {
+ try {
+ Thread.sleep(1);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ continue;
+ }
+ if (lastMessageTimestamp.compareAndSet(last, timestamp)) {
+ return timestamp;
+ }
+ }
+ }
+
private SendMessageResults sendMessage(
- SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients, boolean notifySelf
+ SignalServiceDataMessage.Builder messageBuilder,
+ Set<RecipientIdentifier> recipients,
+ boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
}
Optional<Long> editTargetTimestamp
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
- long timestamp = System.currentTimeMillis();
+ long timestamp = getNextMessageTimestamp();
messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.NoteToSelf || (
}
private SendMessageResults sendTypingMessage(
- SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
+ SignalServiceTypingMessage.Action action,
+ Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
- final var timestamp = System.currentTimeMillis();
+ final var timestamp = getNextMessageTimestamp();
for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
@Override
public SendMessageResults sendTypingMessage(
- TypingAction action, Set<RecipientIdentifier> recipients
+ TypingAction action,
+ Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendTypingMessage(action.toSignalService(), recipients);
}
@Override
- public SendMessageResults sendReadReceipt(
- RecipientIdentifier.Single sender, List<Long> messageIds
- ) {
- final var timestamp = System.currentTimeMillis();
+ public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
+ final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds,
timestamp);
}
@Override
- public SendMessageResults sendViewedReceipt(
- RecipientIdentifier.Single sender, List<Long> messageIds
- ) {
- final var timestamp = System.currentTimeMillis();
+ public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
+ final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
timestamp);
final SignalServiceReceiptMessage receiptMessage
) {
try {
- final var result = context.getSendHelper()
- .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender));
+ final var recipientId = context.getRecipientHelper().resolveRecipient(sender);
+ final var result = context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
+
+ final var serviceId = account.getRecipientAddressResolver()
+ .resolveRecipientAddress(recipientId)
+ .serviceId();
+ if (serviceId.isPresent()) {
+ context.getSyncHelper().sendSyncReceiptMessage(serviceId.get(), receiptMessage);
+ }
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
} catch (UnregisteredRecipientException e) {
return new SendMessageResults(timestamp,
@Override
public SendMessageResults sendMessage(
- Message message, Set<RecipientIdentifier> recipients, boolean notifySelf
+ Message message,
+ Set<RecipientIdentifier> recipients,
+ boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var selfProfile = context.getProfileHelper().getSelfProfile();
if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
@Override
public SendMessageResults sendEditMessage(
- Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
+ Message message,
+ Set<RecipientIdentifier> recipients,
+ long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message);
}
private void applyMessage(
- final SignalServiceDataMessage.Builder messageBuilder, final Message message
+ final SignalServiceDataMessage.Builder messageBuilder,
+ final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
- if (message.messageText().length() > 2000) {
- final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
- final var textAttachment = AttachmentUtils.createAttachmentStream(new StreamDetails(new ByteArrayInputStream(
- messageBytes), MimeUtils.LONG_TEXT, messageBytes.length), Optional.empty());
- messageBuilder.withBody(message.messageText().substring(0, 2000));
- messageBuilder.withAttachment(context.getAttachmentHelper().uploadAttachment(textAttachment));
+ final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
+ if (Utf8.size(message.messageText()) > MAX_MESSAGE_SIZE_BYTES) {
+ final var result = splitByByteLength(message.messageText(), MAX_MESSAGE_SIZE_BYTES);
+ final var trimmed = result.getFirst();
+ final var remainder = result.getSecond();
+ if (remainder != null) {
+ final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
+ final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
+ final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
+ MimeUtils.LONG_TEXT,
+ messageBytes.length);
+ final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
+ Optional.empty(),
+ uploadSpec);
+ messageBuilder.withBody(trimmed);
+ additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
+ } else {
+ messageBuilder.withBody(message.messageText());
+ }
} else {
messageBuilder.withBody(message.messageText());
}
if (!message.attachments().isEmpty()) {
- messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(message.attachments()));
+ final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments());
+ if (!additionalAttachments.isEmpty()) {
+ additionalAttachments.addAll(uploadedAttachments);
+ messageBuilder.withAttachments(additionalAttachments);
+ } else {
+ messageBuilder.withAttachments(uploadedAttachments);
+ }
+ } else if (!additionalAttachments.isEmpty()) {
+ messageBuilder.withAttachments(additionalAttachments);
}
if (!message.mentions().isEmpty()) {
messageBuilder.withMentions(resolveMentions(message.mentions()));
if (streamDetails == null) {
throw new InvalidStickerException("Missing local sticker file");
}
+ final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
+ final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
+ Optional.empty(),
+ uploadSpec);
messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(),
stickerPack.packKey(),
stickerId,
manifestSticker.emoji(),
- AttachmentUtils.createAttachmentStream(streamDetails, Optional.empty())));
+ stickerAttachment));
}
if (!message.previews().isEmpty()) {
final var previews = new ArrayList<SignalServicePreview>(message.previews().size());
@Override
public SendMessageResults sendRemoteDeleteMessage(
- long targetSentTimestamp, Set<RecipientIdentifier> recipients
+ long targetSentTimestamp,
+ Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
if (recipient instanceof RecipientIdentifier.Uuid u) {
account.getMessageSendLogStore()
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
+ } else if (recipient instanceof RecipientIdentifier.Pni pni) {
+ account.getMessageSendLogStore()
+ .deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni()));
} else if (recipient instanceof RecipientIdentifier.Single r) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
@Override
public SendMessageResults sendPaymentNotificationMessage(
- byte[] receipt, String note, RecipientIdentifier.Single recipient
+ byte[] receipt,
+ String note,
+ RecipientIdentifier.Single recipient
) throws IOException {
final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
}
}
+ @Override
+ public SendMessageResults sendMessageRequestResponse(
+ final MessageRequestResponse.Type type,
+ final Set<RecipientIdentifier> recipients
+ ) {
+ var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
+ for (final var recipient : recipients) {
+ if (recipient instanceof RecipientIdentifier.NoteToSelf || (
+ recipient instanceof RecipientIdentifier.Single single
+ && new RecipientAddress(single.toPartialRecipientAddress()).matches(account.getSelfRecipientAddress())
+ )) {
+ final var result = context.getSyncHelper()
+ .sendMessageRequestResponse(type, account.getSelfRecipientId());
+ if (result != null) {
+ results.put(recipient, List.of(toSendMessageResult(result)));
+ }
+ results.put(recipient, List.of(toSendMessageResult(result)));
+ } else if (recipient instanceof RecipientIdentifier.Single single) {
+ try {
+ final var recipientId = context.getRecipientHelper().resolveRecipient(single);
+ final var result = context.getSyncHelper().sendMessageRequestResponse(type, recipientId);
+ if (result != null) {
+ results.put(recipient, List.of(toSendMessageResult(result)));
+ }
+ } catch (UnregisteredRecipientException e) {
+ results.put(recipient,
+ List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
+ }
+ } else if (recipient instanceof RecipientIdentifier.Group group) {
+ final var result = context.getSyncHelper().sendMessageRequestResponse(type, group.groupId());
+ results.put(recipient, List.of(toSendMessageResult(result)));
+ }
+ }
+ return new SendMessageResults(0, results);
+ }
+
@Override
public void hideRecipient(final RecipientIdentifier.Single recipient) {
final var recipientIdOptional = context.getRecipientHelper().resolveRecipientOptional(recipient);
@Override
public void setContactName(
- RecipientIdentifier.Single recipient, String givenName, final String familyName
+ final RecipientIdentifier.Single recipient,
+ final String givenName,
+ final String familyName,
+ final String nickGivenName,
+ final String nickFamilyName,
+ final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
context.getContactHelper()
- .setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName);
+ .setContactName(context.getRecipientHelper().resolveRecipient(recipient),
+ givenName,
+ familyName,
+ nickGivenName,
+ nickFamilyName,
+ note);
syncRemoteStorage();
}
@Override
public void setContactsBlocked(
- Collection<RecipientIdentifier.Single> recipients, boolean blocked
+ Collection<RecipientIdentifier.Single> recipients,
+ boolean blocked
) throws IOException, UnregisteredRecipientException {
if (recipients.isEmpty()) {
return;
continue;
}
context.getContactHelper().setContactBlocked(recipientId, blocked);
+ context.getSyncHelper()
+ .sendMessageRequestResponse(blocked
+ ? MessageRequestResponse.Type.BLOCK
+ : MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT, recipientId);
// if we don't have a common group with the blocked contact we need to rotate the profile key
shouldRotateProfileKey = blocked && (
shouldRotateProfileKey || account.getGroupStore()
@Override
public void setGroupsBlocked(
- final Collection<GroupId> groupIds, final boolean blocked
+ final Collection<GroupId> groupIds,
+ final boolean blocked
) throws GroupNotFoundException, IOException {
if (groupIds.isEmpty()) {
return;
continue;
}
context.getGroupHelper().setGroupBlocked(groupId, blocked);
+ context.getSyncHelper()
+ .sendMessageRequestResponse(blocked
+ ? MessageRequestResponse.Type.BLOCK
+ : MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT, groupId);
shouldRotateProfileKey = blocked;
}
if (shouldRotateProfileKey) {
@Override
public void setExpirationTimer(
- RecipientIdentifier.Single recipient, int messageExpirationTimer
+ RecipientIdentifier.Single recipient,
+ int messageExpirationTimer
) throws IOException, UnregisteredRecipientException {
var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
@Override
public void receiveMessages(
- Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
+ Optional<Duration> timeout,
+ Optional<Integer> maxMessages,
+ ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException {
receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
}
}
private void receiveMessages(
- Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler
+ Duration timeout,
+ boolean returnOnTimeout,
+ Integer maxMessages,
+ ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException {
synchronized (messageHandlers) {
if (isReceiving()) {
s.getContact(),
s.getProfileKey(),
s.getExpiringProfileKeyCredential(),
- s.getProfile()))
+ s.getProfile(),
+ s.getDiscoverable()))
.toList();
}
final var scannableFingerprint = context.getIdentityHelper()
.computeSafetyNumberForScanning(identityInfo.getServiceId(), identityInfo.getIdentityKey());
return new Identity(address.toApiRecipientAddress(),
- identityInfo.getIdentityKey(),
+ identityInfo.getIdentityKey().getPublicKey().serialize(),
context.getIdentityHelper()
.computeSafetyNumber(identityInfo.getServiceId(), identityInfo.getIdentityKey()),
scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
@Override
public boolean trustIdentityVerified(
- RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
+ RecipientIdentifier.Single recipient,
+ IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException {
return switch (verificationCode) {
case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient,
}
private boolean trustIdentity(
- RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
+ RecipientIdentifier.Single recipient,
+ Function<RecipientId, Boolean> trustMethod
) throws UnregisteredRecipientException {
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
final var updated = trustMethod.apply(recipientId);
if (updated && this.isReceiving()) {
- context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
+ account.setNeedsToRetryFailedMessages(true);
}
return updated;
}
return context.getAttachmentHelper().retrieveAttachment(id).getStream();
}
+ @Override
+ public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
+ final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
+ final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
+ if (streamDetails == null) {
+ throw new FileNotFoundException();
+ }
+ return streamDetails.getStream();
+ }
+
+ @Override
+ public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
+ context.getProfileHelper().getRecipientProfile(recipientId);
+ final var address = account.getRecipientStore().resolveRecipientAddress(recipientId);
+ final var streamDetails = context.getAvatarStore().retrieveProfileAvatar(address);
+ if (streamDetails == null) {
+ throw new FileNotFoundException();
+ }
+ return streamDetails.getStream();
+ }
+
+ @Override
+ public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException {
+ final var streamDetails = context.getAvatarStore().retrieveGroupAvatar(groupId);
+ context.getGroupHelper().getGroup(groupId);
+ if (streamDetails == null) {
+ throw new FileNotFoundException();
+ }
+ return streamDetails.getStream();
+ }
+
+ @Override
+ public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException {
+ var streamDetails = context.getStickerPackStore().retrieveSticker(stickerPackId, stickerId);
+ if (streamDetails == null) {
+ final var pack = account.getStickerStore().getStickerPack(stickerPackId);
+ if (pack != null) {
+ try {
+ context.getStickerHelper().retrieveStickerPack(stickerPackId, pack.packKey());
+ } catch (InvalidMessageException e) {
+ logger.warn("Failed to download sticker pack");
+ }
+ }
+ }
+ if (streamDetails == null) {
+ throw new FileNotFoundException();
+ }
+ return streamDetails.getStream();
+ }
+
@Override
public void close() {
Thread thread;
if (thread != null) {
stopReceiveThread(thread);
}
- executor.close();
context.close();
+ executor.close();
- dependencies.getSignalWebSocket().disconnect();
+ dependencies.getAuthenticatedSignalWebSocket().disconnect();
+ dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close();
disposable.dispose();