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.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.UsernameLinkUrl;
import org.asamk.signal.manager.api.UsernameStatus;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
-import org.asamk.signal.manager.config.ServiceConfig;
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.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 Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) {
+ public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
final var registeredUsers = new HashMap<String, RecipientAddress>();
for (final var username : usernames) {
try {
String newNumber,
String verificationCode,
String pin
- ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
+ ) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
if (!account.isPrimaryDevice()) {
throw new NotPrimaryDeviceException();
}
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 -> {
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,
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 || (
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 sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
- final var timestamp = System.currentTimeMillis();
+ 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();
+ final var timestamp = getNextMessageTimestamp();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
timestamp);
final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
- if (message.messageText().length() > ServiceConfig.MAX_MESSAGE_BODY_SIZE) {
- 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(message.messageText().substring(0, ServiceConfig.MAX_MESSAGE_BODY_SIZE));
- 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());
}
.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 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();
}
context.close();
executor.close();
- dependencies.getSignalWebSocket().disconnect();
+ dependencies.getAuthenticatedSignalWebSocket().disconnect();
+ dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close();
disposable.dispose();