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.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.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.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(),
@Override
public void startChangeNumber(
- String newNumber, boolean voiceVerification, String captcha
+ 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
+ String newNumber,
+ String verificationCode,
+ String pin
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
@Override
public void submitRateLimitRecaptchaChallenge(
- String challenge, String captcha
+ String challenge,
+ String captcha
) throws IOException, CaptchaRejectedException {
- captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+ captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
try {
- dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
+ 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 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);
@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 {
final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
- if (message.messageText().length() > 2000) {
- final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
- final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec().toProto();
- final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
- MimeUtils.LONG_TEXT,
- messageBytes.length);
- final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
- Optional.empty(),
- uploadSpec);
- messageBuilder.withBody(message.messageText().substring(0, 2000));
- additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
+ 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()) {
+ final var uploadedAttachments = context.getAttachmentHelper().uploadAttachments(message.attachments());
if (!additionalAttachments.isEmpty()) {
- additionalAttachments.addAll(context.getAttachmentHelper().uploadAttachments(message.attachments()));
+ additionalAttachments.addAll(uploadedAttachments);
messageBuilder.withAttachments(additionalAttachments);
} else {
- messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(message.attachments()));
+ 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().toProto();
+ final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
final var stickerAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
Optional.empty(),
uploadSpec);
@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);
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
} else if (recipient instanceof RecipientIdentifier.Pni pni) {
account.getMessageSendLogStore()
- .deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.parseOrThrow(pni.pni()));
+ .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
+ final MessageRequestResponse.Type type,
+ final Set<RecipientIdentifier> recipients
) {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
for (final var recipient : recipients) {
@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;
@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;
@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()) {
@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);
context.close();
executor.close();
- dependencies.getSignalWebSocket().disconnect();
+ dependencies.getAuthenticatedSignalWebSocket().disconnect();
+ dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close();
disposable.dispose();