mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-20 06:09:21 +00:00
Compare commits
13 Commits
1353da1d12
...
4e56fe839b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e56fe839b | ||
|
|
78dba20a59 | ||
|
|
b48ccf2605 | ||
|
|
54d713ea7d | ||
|
|
4dda7582a3 | ||
|
|
248c0c0dab | ||
|
|
e70bddd790 | ||
|
|
ac5ed431d3 | ||
|
|
a041826cd5 | ||
|
|
712c9ea741 | ||
|
|
8b74f0653d | ||
|
|
a770b03fe6 | ||
|
|
1cce283aca |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@ -75,6 +75,16 @@ jobs:
|
||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
- name: Upload JSON schemas archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz
|
||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz
|
||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
||||
|
||||
build-container:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- New `sendStory` command to post file attachment stories to "My Story"
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sending to large groups is no longer slowed down by members that are already known to be unregistered; they are skipped instead of being retried via the legacy 1:1 send path on every send.
|
||||
|
||||
@ -54,6 +54,7 @@ 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.signal.core.util.UuidUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -74,6 +75,10 @@ public interface Manager extends Closeable {
|
||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||
}
|
||||
|
||||
static boolean isValidAci(final String aci) {
|
||||
return UuidUtil.INSTANCE.isUuid(aci);
|
||||
}
|
||||
|
||||
static boolean isSignalClientAvailable() {
|
||||
final Logger logger = LoggerFactory.getLogger(Manager.class);
|
||||
try {
|
||||
@ -91,6 +96,8 @@ public interface Manager extends Closeable {
|
||||
|
||||
String getSelfNumber();
|
||||
|
||||
String getSelfACI();
|
||||
|
||||
/**
|
||||
* This is used for checking a set of phone numbers for registration on Signal
|
||||
*
|
||||
@ -213,6 +220,14 @@ public interface Manager extends Closeable {
|
||||
long editTargetTimestamp
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
/**
|
||||
* Post a file attachment story to "My Story".
|
||||
*
|
||||
* @param attachment path to the file to upload and post as a story
|
||||
* @param allowsReplies whether other users are allowed to reply to this story
|
||||
*/
|
||||
SendMessageResults sendStory(String attachment, boolean allowsReplies) throws IOException, AttachmentInvalidException;
|
||||
|
||||
SendMessageResults sendRemoteDeleteMessage(
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients
|
||||
|
||||
@ -15,6 +15,7 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
@ -67,7 +68,7 @@ public class SignalAccountFiles {
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
try {
|
||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return null;
|
||||
@ -90,15 +91,31 @@ public class SignalAccountFiles {
|
||||
return new MultiAccountManagerImpl(managers, this);
|
||||
}
|
||||
|
||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var accountPath = accountsStore.getPathByNumber(number);
|
||||
return this.initManager(number, accountPath);
|
||||
return this.initManagerByNumber(number, accountPath);
|
||||
}
|
||||
|
||||
private Manager initManager(
|
||||
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var aci = ACI.parseOrThrow(aciStr);
|
||||
final var accountPath = accountsStore.getPathByAci(aci);
|
||||
return this.initManagerByAci(aci, accountPath);
|
||||
}
|
||||
|
||||
private Manager initManagerByNumber(
|
||||
String number,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var account = loadAccount(accountPath);
|
||||
if (!number.equals(account.getNumber())) {
|
||||
account.close();
|
||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||
}
|
||||
|
||||
return initManagerFromAccount(number, accountPath, account);
|
||||
}
|
||||
|
||||
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
|
||||
if (accountPath == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
@ -106,12 +123,27 @@ public class SignalAccountFiles {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
|
||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
if (!number.equals(account.getNumber())) {
|
||||
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||
}
|
||||
|
||||
private Manager initManagerByAci(
|
||||
ACI aci,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
final var account = loadAccount(accountPath);
|
||||
if (!aci.equals(account.getAci())) {
|
||||
account.close();
|
||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
||||
}
|
||||
|
||||
return initManagerFromAccount(aci.toString(), accountPath, account);
|
||||
}
|
||||
|
||||
private ManagerImpl initManagerFromAccount(
|
||||
final String identifier,
|
||||
final String accountPath,
|
||||
final SignalAccount account
|
||||
) throws NotRegisteredException, IOException, AccountCheckException {
|
||||
if (!account.isRegistered()) {
|
||||
account.close();
|
||||
throw new NotRegisteredException();
|
||||
@ -136,7 +168,7 @@ public class SignalAccountFiles {
|
||||
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
||||
} catch (IOException e) {
|
||||
manager.close();
|
||||
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
||||
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (account.getServiceEnvironment() == null) {
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package org.asamk.signal.manager.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class InvalidEnvelopeContentException extends Exception {
|
||||
|
||||
public static final String INVALID_ENVELOPE_CONTENT = "INVALID_ENVELOPE_CONTENT";
|
||||
public static final String DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS = "DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS";
|
||||
|
||||
private final String code;
|
||||
private final String sender;
|
||||
private final int senderDevice;
|
||||
private final Integer bodyLength;
|
||||
private final List<InvalidBodyRange> invalidBodyRanges;
|
||||
|
||||
public InvalidEnvelopeContentException(
|
||||
final String message,
|
||||
final String code,
|
||||
final String sender,
|
||||
final int senderDevice,
|
||||
final Integer bodyLength,
|
||||
final List<InvalidBodyRange> invalidBodyRanges,
|
||||
final Throwable cause
|
||||
) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
this.bodyLength = bodyLength;
|
||||
this.invalidBodyRanges = List.copyOf(invalidBodyRanges);
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public int getSenderDevice() {
|
||||
return senderDevice;
|
||||
}
|
||||
|
||||
public Integer getBodyLength() {
|
||||
return bodyLength;
|
||||
}
|
||||
|
||||
public List<InvalidBodyRange> getInvalidBodyRanges() {
|
||||
return invalidBodyRanges;
|
||||
}
|
||||
|
||||
public record InvalidBodyRange(int index, Integer start, Integer length, String type) {}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@ -1032,6 +1033,8 @@ public record MessageEnvelope(
|
||||
? recipientResolver.resolveRecipient(serviceId)
|
||||
: envelope.isUnidentifiedSender() && content != null
|
||||
? recipientResolver.resolveRecipient(content.getSender())
|
||||
: exception instanceof InvalidEnvelopeContentException e && e.getSender() != null
|
||||
? recipientResolver.resolveRecipient(ServiceId.parseOrThrow(e.getSender()))
|
||||
: exception instanceof ProtocolException e
|
||||
? recipientResolver.resolveRecipient(e.getSender())
|
||||
: null;
|
||||
@ -1039,6 +1042,7 @@ public record MessageEnvelope(
|
||||
? envelope.getSourceDevice()
|
||||
: content != null
|
||||
? content.getSenderDevice()
|
||||
: exception instanceof InvalidEnvelopeContentException e ? e.getSenderDevice()
|
||||
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
|
||||
|
||||
Optional<Receipt> receipt;
|
||||
|
||||
@ -21,6 +21,7 @@ import org.asamk.signal.manager.actions.SyncStorageDataAction;
|
||||
import org.asamk.signal.manager.actions.UpdateAccountAttributesAction;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||
@ -31,6 +32,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
@ -48,10 +50,12 @@ import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult;
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
|
||||
@ -69,7 +73,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.Content;
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage;
|
||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||
import org.whispersystems.signalservice.internal.push.GroupContext;
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -85,6 +94,12 @@ import java.util.stream.Collectors;
|
||||
public final class IncomingMessageHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class);
|
||||
private static final String DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
|
||||
"[DataMessage] Body range with out-of-bounds start/length!";
|
||||
private static final String DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
|
||||
"[DataMessage] Quote body range with out-of-bounds start/length!";
|
||||
private static final String EDIT_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
|
||||
"[EditMessage] Body range with out-of-bounds start/length!";
|
||||
|
||||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
@ -107,6 +122,8 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
|
||||
SignalServiceContent content = null;
|
||||
Content decryptedContent = null;
|
||||
InvalidEnvelopeContentException validationException = null;
|
||||
if (!envelope.isReceipt()) {
|
||||
account.getIdentityKeyStore().setRetryingDecryption(true);
|
||||
try {
|
||||
@ -114,6 +131,7 @@ public final class IncomingMessageHandler {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
decryptedContent = cipherResult.getContent();
|
||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
@ -124,14 +142,21 @@ public final class IncomingMessageHandler {
|
||||
.resolveRecipientAddress(recipientId)
|
||||
.toApiRecipientAddress(), e.getSenderDevice());
|
||||
return new Pair<>(List.of(), exception);
|
||||
} catch (InvalidEnvelopeContentException e) {
|
||||
validationException = e;
|
||||
} catch (Exception e) {
|
||||
return new Pair<>(List.of(), e);
|
||||
} finally {
|
||||
account.getIdentityKeyStore().setRetryingDecryption(false);
|
||||
}
|
||||
}
|
||||
actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, null));
|
||||
return new Pair<>(actions, null);
|
||||
actions.addAll(checkAndHandleMessage(envelope,
|
||||
content,
|
||||
validationException == null ? null : decryptedContent,
|
||||
receiveConfig,
|
||||
handler,
|
||||
validationException));
|
||||
return new Pair<>(actions, validationException);
|
||||
}
|
||||
|
||||
public Pair<List<HandleAction>, Exception> handleEnvelope(
|
||||
@ -144,6 +169,7 @@ public final class IncomingMessageHandler {
|
||||
actions.add(RefreshPreKeysAction.create());
|
||||
}
|
||||
SignalServiceContent content = null;
|
||||
Content decryptedContent = null;
|
||||
Exception exception = null;
|
||||
if (envelope.getSourceServiceId() != null) {
|
||||
// Store uuid if we don't have it already
|
||||
@ -167,6 +193,7 @@ public final class IncomingMessageHandler {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
decryptedContent = cipherResult.getContent();
|
||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
@ -217,7 +244,12 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, exception));
|
||||
actions.addAll(checkAndHandleMessage(envelope,
|
||||
content,
|
||||
exception instanceof InvalidEnvelopeContentException ? decryptedContent : null,
|
||||
receiveConfig,
|
||||
handler,
|
||||
exception));
|
||||
return new Pair<>(actions, exception);
|
||||
}
|
||||
|
||||
@ -225,7 +257,8 @@ public final class IncomingMessageHandler {
|
||||
Envelope envelope,
|
||||
SignalServiceCipherResult cipherResult,
|
||||
long serverDeliveredTimestamp
|
||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
|
||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException,
|
||||
InvalidMessageStructureException, InvalidEnvelopeContentException {
|
||||
final var content = cipherResult.getContent();
|
||||
final var envelopeMetadata = cipherResult.getMetadata();
|
||||
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope,
|
||||
@ -234,8 +267,17 @@ public final class IncomingMessageHandler {
|
||||
cipherResult.getMetadata().getCiphertextMessageType());
|
||||
|
||||
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
|
||||
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
|
||||
return null;
|
||||
final var exception = createInvalidEnvelopeContentException(v, envelopeMetadata, content);
|
||||
logger.warn("Invalid content! reason={} code={} source={} sourceDevice={} timestamp={} bodyLength={} invalidBodyRanges={}",
|
||||
exception.getMessage(),
|
||||
exception.getCode(),
|
||||
exception.getSender(),
|
||||
exception.getSenderDevice(),
|
||||
envelope.clientTimestamp,
|
||||
exception.getBodyLength(),
|
||||
exception.getInvalidBodyRanges());
|
||||
logger.debug("Invalid content validation location", v.getThrowable());
|
||||
throw exception;
|
||||
}
|
||||
|
||||
if (validationResult instanceof EnvelopeContentValidator.Result.UnsupportedDataMessage v) {
|
||||
@ -252,9 +294,105 @@ public final class IncomingMessageHandler {
|
||||
serverDeliveredTimestamp);
|
||||
}
|
||||
|
||||
static InvalidEnvelopeContentException createInvalidEnvelopeContentException(
|
||||
final EnvelopeContentValidator.Result.Invalid validationResult,
|
||||
final EnvelopeMetadata envelopeMetadata,
|
||||
final Content content
|
||||
) {
|
||||
final var reason = validationResult.getReason();
|
||||
final var dataMessage = getDataMessage(content, reason);
|
||||
final String body;
|
||||
final List<BodyRange> bodyRanges;
|
||||
if (DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)) {
|
||||
if (dataMessage == null || dataMessage.quote == null) {
|
||||
body = null;
|
||||
bodyRanges = null;
|
||||
} else {
|
||||
body = dataMessage.quote.text;
|
||||
bodyRanges = dataMessage.quote.bodyRanges;
|
||||
}
|
||||
} else if (dataMessage == null) {
|
||||
body = null;
|
||||
bodyRanges = null;
|
||||
} else {
|
||||
body = dataMessage.body;
|
||||
bodyRanges = dataMessage.bodyRanges;
|
||||
}
|
||||
|
||||
final Integer bodyLength = bodyRanges == null ? null : body == null ? 0 : body.length();
|
||||
final List<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges = new ArrayList<>();
|
||||
if (bodyRanges != null) {
|
||||
for (int i = 0; i < bodyRanges.size(); i++) {
|
||||
final var range = bodyRanges.get(i);
|
||||
final long start = range.start == null ? 0 : range.start;
|
||||
final long length = range.length == null ? 0 : range.length;
|
||||
if (start < 0 || length < 0 || start + length > bodyLength) {
|
||||
invalidBodyRanges.add(new InvalidEnvelopeContentException.InvalidBodyRange(i,
|
||||
range.start,
|
||||
range.length,
|
||||
getBodyRangeType(range)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final var code = isBodyRangeOutOfBoundsReason(reason)
|
||||
? InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS
|
||||
: InvalidEnvelopeContentException.INVALID_ENVELOPE_CONTENT;
|
||||
return new InvalidEnvelopeContentException(reason,
|
||||
code,
|
||||
envelopeMetadata.getSourceServiceId().toString(),
|
||||
envelopeMetadata.getSourceDeviceId(),
|
||||
bodyLength,
|
||||
invalidBodyRanges,
|
||||
validationResult.getThrowable());
|
||||
}
|
||||
|
||||
private static DataMessage getDataMessage(final Content content, final String validationReason) {
|
||||
if (validationReason.startsWith("[EditMessage]")) {
|
||||
return getEditDataMessage(content);
|
||||
}
|
||||
|
||||
if (content.dataMessage != null) {
|
||||
return content.dataMessage;
|
||||
}
|
||||
if (content.syncMessage != null && content.syncMessage.sent != null
|
||||
&& content.syncMessage.sent.message != null) {
|
||||
return content.syncMessage.sent.message;
|
||||
}
|
||||
return getEditDataMessage(content);
|
||||
}
|
||||
|
||||
private static DataMessage getEditDataMessage(final Content content) {
|
||||
if (content.editMessage != null) {
|
||||
return content.editMessage.dataMessage;
|
||||
}
|
||||
if (content.syncMessage != null && content.syncMessage.sent != null
|
||||
&& content.syncMessage.sent.editMessage != null) {
|
||||
return content.syncMessage.sent.editMessage.dataMessage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isBodyRangeOutOfBoundsReason(final String reason) {
|
||||
return DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)
|
||||
|| DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)
|
||||
|| EDIT_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason);
|
||||
}
|
||||
|
||||
private static String getBodyRangeType(final BodyRange range) {
|
||||
if (range.style != null) {
|
||||
return "STYLE_" + range.style.name();
|
||||
}
|
||||
if (range.mentionAci != null || range.mentionAciBinary != null) {
|
||||
return "MENTION";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
private List<HandleAction> checkAndHandleMessage(
|
||||
final SignalServiceEnvelope envelope,
|
||||
final SignalServiceContent content,
|
||||
final Content invalidContent,
|
||||
final ReceiveConfig receiveConfig,
|
||||
final Manager.ReceiveMessageHandler handler,
|
||||
final Exception exception
|
||||
@ -282,19 +420,21 @@ public final class IncomingMessageHandler {
|
||||
account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId);
|
||||
}
|
||||
|
||||
var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content);
|
||||
final var groupFilterInfo = getGroupFilterInfo(content, invalidContent, exception);
|
||||
var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content, exception, groupFilterInfo);
|
||||
final var groupContext = getGroupContext(content);
|
||||
if (groupContext != null && groupContext.getGroupV2().isPresent()) {
|
||||
handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars());
|
||||
}
|
||||
// Check again in case the user just joined the group
|
||||
notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content);
|
||||
notAllowedToSendToGroup = notAllowedToSendToGroup
|
||||
&& isNotAllowedToSendToGroup(envelope, content, exception, groupFilterInfo);
|
||||
|
||||
if (isMessageBlocked(envelope, content)) {
|
||||
if (isMessageBlocked(envelope, content, exception, groupFilterInfo)) {
|
||||
logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
|
||||
return List.of();
|
||||
} else if (notAllowedToSendToGroup) {
|
||||
final var senderAddress = getSenderAddress(envelope, content);
|
||||
final var senderAddress = getSenderAddress(envelope, content, exception);
|
||||
logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
|
||||
senderAddress == null ? null : senderAddress.getIdentifier(),
|
||||
envelope.getTimestamp());
|
||||
@ -733,7 +873,7 @@ public final class IncomingMessageHandler {
|
||||
return new Pair<>(actions, longTexts);
|
||||
}
|
||||
|
||||
private SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
|
||||
private static SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
@ -759,51 +899,62 @@ public final class IncomingMessageHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(source);
|
||||
if (context.getContactHelper().isContactBlocked(recipientId)) {
|
||||
return true;
|
||||
private boolean isMessageBlocked(
|
||||
SignalServiceEnvelope envelope,
|
||||
SignalServiceContent content,
|
||||
Exception exception,
|
||||
GroupFilterInfo groupFilterInfo
|
||||
) {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content, exception);
|
||||
if (source != null) {
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(source);
|
||||
if (context.getContactHelper().isContactBlocked(recipientId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final var groupContext = getGroupContext(content);
|
||||
if (groupContext != null) {
|
||||
var groupId = GroupUtils.getGroupId(groupContext);
|
||||
return context.getGroupHelper().isGroupBlocked(groupId);
|
||||
if (groupFilterInfo != null) {
|
||||
return isGroupBlocked(context.getGroupHelper().getGroup(groupFilterInfo.groupId()));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content);
|
||||
private boolean isNotAllowedToSendToGroup(
|
||||
SignalServiceEnvelope envelope,
|
||||
SignalServiceContent content,
|
||||
Exception exception,
|
||||
GroupFilterInfo groupFilterInfo
|
||||
) {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content, exception);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final var groupContext = getGroupContext(content);
|
||||
if (groupContext == null) {
|
||||
if (groupFilterInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groupContext.getGroupV1().isPresent()) {
|
||||
var groupInfo = groupContext.getGroupV1().get();
|
||||
if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
|
||||
return false;
|
||||
}
|
||||
if (groupFilterInfo.isQuit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final var message = content.getDataMessage().orElse(null);
|
||||
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(source);
|
||||
|
||||
final var groupId = GroupUtils.getGroupId(groupContext);
|
||||
final var group = context.getGroupHelper().getGroup(groupId);
|
||||
final var group = context.getGroupHelper().getGroup(groupFilterInfo.groupId());
|
||||
return isNotAllowedToSendToGroup(group, recipientId, groupFilterInfo);
|
||||
}
|
||||
|
||||
if (message != null && message.getAdminDelete().isPresent() && (group == null || !group.isAdmin(recipientId))) {
|
||||
static boolean isGroupBlocked(final GroupInfo group) {
|
||||
return group != null && group.isBlocked();
|
||||
}
|
||||
|
||||
static boolean isNotAllowedToSendToGroup(
|
||||
final GroupInfo group,
|
||||
final RecipientId recipientId,
|
||||
final GroupFilterInfo groupFilterInfo
|
||||
) {
|
||||
if (groupFilterInfo.hasAdminDelete() && (group == null || !group.isAdmin(recipientId))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -811,24 +962,102 @@ public final class IncomingMessageHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!group.isMember(recipientId) && !(
|
||||
group.isPendingMember(recipientId) && message != null && message.isGroupV2Update()
|
||||
)) {
|
||||
if (!group.isMember(recipientId)
|
||||
&& !(group.isPendingMember(recipientId) && groupFilterInfo.isGroupV2Update())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
||||
return message == null
|
||||
|| message.getBody().isPresent()
|
||||
|| message.getAttachments().isPresent()
|
||||
|| message.getQuote().isPresent()
|
||||
|| message.getPreviews().isPresent()
|
||||
|| message.getMentions().isPresent()
|
||||
|| message.getSticker().isPresent();
|
||||
return groupFilterInfo.hasAnnouncementContent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static GroupFilterInfo getGroupFilterInfo(
|
||||
final SignalServiceContent content,
|
||||
final Content invalidContent,
|
||||
final Exception exception
|
||||
) {
|
||||
if (content != null) {
|
||||
final var groupContext = getGroupContext(content);
|
||||
if (groupContext == null) {
|
||||
return null;
|
||||
}
|
||||
final var message = content.getDataMessage().orElse(null);
|
||||
return new GroupFilterInfo(GroupUtils.getGroupId(groupContext),
|
||||
groupContext.getGroupV1()
|
||||
.map(group -> group.getType() == SignalServiceGroup.Type.QUIT)
|
||||
.orElse(false),
|
||||
message != null && message.getAdminDelete().isPresent(),
|
||||
message != null && message.isGroupV2Update(),
|
||||
message == null
|
||||
|| message.getBody().isPresent()
|
||||
|| message.getAttachments().isPresent()
|
||||
|| message.getQuote().isPresent()
|
||||
|| message.getPreviews().isPresent()
|
||||
|| message.getMentions().isPresent()
|
||||
|| message.getSticker().isPresent());
|
||||
}
|
||||
|
||||
if (invalidContent == null || !(exception instanceof InvalidEnvelopeContentException e)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final var message = getDataMessage(invalidContent, e.getMessage());
|
||||
if (message != null) {
|
||||
final var groupId = getGroupId(message);
|
||||
if (groupId == null) {
|
||||
return null;
|
||||
}
|
||||
return new GroupFilterInfo(groupId,
|
||||
message.group != null
|
||||
&& message.group.type == GroupContext.Type.QUIT,
|
||||
message.adminDelete != null,
|
||||
message.groupV2 != null
|
||||
&& message.groupV2.groupChange != null
|
||||
&& message.groupV2.groupChange.size() > 0,
|
||||
message.body != null
|
||||
|| !message.attachments.isEmpty()
|
||||
|| message.quote != null
|
||||
|| !message.preview.isEmpty()
|
||||
|| message.bodyRanges.stream()
|
||||
.anyMatch(range -> range.mentionAci != null || range.mentionAciBinary != null)
|
||||
|| message.sticker != null);
|
||||
}
|
||||
|
||||
if (invalidContent.storyMessage != null && invalidContent.storyMessage.group != null) {
|
||||
final var groupId = getGroupId(invalidContent.storyMessage.group);
|
||||
return groupId == null ? null : new GroupFilterInfo(groupId, false, false, false, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static GroupId getGroupId(final DataMessage message) {
|
||||
if (message.group != null && message.group.id != null) {
|
||||
return GroupId.v1(message.group.id.toByteArray());
|
||||
}
|
||||
return message.groupV2 == null ? null : getGroupId(message.groupV2);
|
||||
}
|
||||
|
||||
private static GroupId getGroupId(final GroupContextV2 groupContext) {
|
||||
if (groupContext.masterKey == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return GroupUtils.getGroupIdV2(new GroupMasterKey(groupContext.masterKey.toByteArray()));
|
||||
} catch (InvalidInputException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
record GroupFilterInfo(
|
||||
GroupId groupId,
|
||||
boolean isQuit,
|
||||
boolean hasAdminDelete,
|
||||
boolean isGroupV2Update,
|
||||
boolean hasAnnouncementContent
|
||||
) {}
|
||||
|
||||
private Pair<List<HandleAction>, Map<String, String>> handleSignalServiceDataMessage(
|
||||
SignalServiceDataMessage message,
|
||||
boolean isSync,
|
||||
@ -1067,7 +1296,10 @@ public final class IncomingMessageHandler {
|
||||
this.account.getProfileStore().storeProfileKey(source, profileKey);
|
||||
}
|
||||
|
||||
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
private static SignalServiceAddress getSenderAddress(
|
||||
final SignalServiceEnvelope envelope,
|
||||
final SignalServiceContent content
|
||||
) {
|
||||
final var serviceId = envelope.getSourceServiceId();
|
||||
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
||||
return new SignalServiceAddress(serviceId);
|
||||
@ -1078,6 +1310,24 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
static SignalServiceAddress getSenderAddress(
|
||||
final SignalServiceEnvelope envelope,
|
||||
final SignalServiceContent content,
|
||||
final Exception exception
|
||||
) {
|
||||
final var source = getSenderAddress(envelope, content);
|
||||
if (source != null) {
|
||||
return source;
|
||||
}
|
||||
if (exception instanceof InvalidEnvelopeContentException e && e.getSender() != null) {
|
||||
final var sender = ServiceId.parseOrNull(e.getSender());
|
||||
if (sender != null) {
|
||||
return new SignalServiceAddress(sender);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
final var serviceId = envelope.getSourceServiceId();
|
||||
if (!envelope.isUnidentifiedSender() && serviceId != null) {
|
||||
|
||||
@ -35,6 +35,8 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
@ -330,6 +332,53 @@ public class SendHelper {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a story message (file attachment) to "My Story".
|
||||
*/
|
||||
public List<SendMessageResult> sendStoryMessage(
|
||||
SignalServiceStoryMessage storyMessage,
|
||||
long timestamp,
|
||||
Set<RecipientId> recipientIds,
|
||||
boolean allowsReplies
|
||||
) throws IOException {
|
||||
final var messageSender = dependencies.getMessageSender();
|
||||
|
||||
final var recipientIdList = List.copyOf(recipientIds);
|
||||
final var addressesMap = recipientIdList.stream()
|
||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||
|
||||
final var addresses = recipientIdList.stream().map(addressesMap::get).toList();
|
||||
final var unidentifiedAccesses = recipientIdList.stream().map(unidentifiedAccessesMap::get).toList();
|
||||
final var storyMessageRecipients = recipientIdList.stream()
|
||||
.map(id -> new SignalServiceStoryMessageRecipient(addressesMap.get(id),
|
||||
List.of(DistributionId.MY_STORY.asUuid().toString()),
|
||||
allowsReplies))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
final List<SendMessageResult> results;
|
||||
try {
|
||||
results = messageSender.sendGroupStory(DistributionId.MY_STORY,
|
||||
Optional.empty(),
|
||||
addresses,
|
||||
unidentifiedAccesses,
|
||||
null,
|
||||
false,
|
||||
storyMessage,
|
||||
timestamp,
|
||||
storyMessageRecipients,
|
||||
null);
|
||||
} catch (UntrustedIdentityException | InvalidKeyException | NoSessionException | InvalidRegistrationIdException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
for (var r : results) {
|
||||
handleSendMessageResult(r);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<SendMessageResult> sendAsGroupMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final GroupInfo g,
|
||||
|
||||
@ -105,10 +105,12 @@ import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
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.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
@ -245,6 +247,11 @@ public class ManagerImpl implements Manager {
|
||||
return account.getNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return account.getAci().toString();
|
||||
}
|
||||
|
||||
public void checkAccountState() throws IOException {
|
||||
context.getAccountHelper().checkAccountState();
|
||||
final var lastRecipientsRefresh = account.getLastRecipientsRefresh();
|
||||
@ -827,6 +834,50 @@ public class ManagerImpl implements Manager {
|
||||
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendStory(
|
||||
String attachment,
|
||||
boolean allowsReplies
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
final var file = new File(attachment);
|
||||
final var mimeType = MimeUtils.getFileMimeType(file);
|
||||
if (mimeType.isEmpty() || (!mimeType.get().startsWith("image/") && !mimeType.get().startsWith("video/"))) {
|
||||
throw new AttachmentInvalidException(attachment,
|
||||
new IOException("Stories only support image and video attachments"));
|
||||
}
|
||||
|
||||
final var recipients = account.getRecipientStore()
|
||||
.getRecipients(true, Optional.of(false), Set.of(), Optional.empty());
|
||||
final var recipientIds = recipients.stream()
|
||||
.filter(r -> !r.getRecipientId().equals(account.getSelfRecipientId()))
|
||||
.filter(r -> r.getContact() != null && !r.getContact().hideStory())
|
||||
.map(r -> r.getRecipientId())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (recipientIds.isEmpty()) {
|
||||
throw new IOException("No eligible contacts found for story delivery");
|
||||
}
|
||||
|
||||
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
||||
final var storyMessage = SignalServiceStoryMessage.forFileAttachment(account.getProfileKey().serialize(),
|
||||
null,
|
||||
uploadedAttachment,
|
||||
allowsReplies,
|
||||
List.of());
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
|
||||
final var sendResults = context.getSendHelper()
|
||||
.sendStoryMessage(storyMessage, timestamp, recipientIds, allowsReplies);
|
||||
|
||||
final var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
for (final var sendResult : sendResults) {
|
||||
final var result = toSendMessageResult(sendResult);
|
||||
results.put(RecipientIdentifier.Single.fromAddress(result.address()), List.of(result));
|
||||
}
|
||||
|
||||
return new SendMessageResults(timestamp, results);
|
||||
}
|
||||
|
||||
private void applyMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final Message message
|
||||
|
||||
@ -7,6 +7,7 @@ import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.SignalAccountFiles;
|
||||
import org.asamk.signal.manager.api.AccountCheckException;
|
||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -95,23 +96,47 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manager getManager(final String number) {
|
||||
public Manager getManager(final String identifier) {
|
||||
synchronized (managers) {
|
||||
final var manager = managers.stream()
|
||||
.filter(m -> m.getSelfNumber().equals(number))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (manager != null) {
|
||||
return manager;
|
||||
}
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManager(number);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (IOException | NotRegisteredException | AccountCheckException e) {
|
||||
logger.warn("Failed to load new manager", e);
|
||||
return null;
|
||||
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||
// Check if UUID corresponds to an already-loaded manager
|
||||
final var existing = managers.stream()
|
||||
.filter(m -> m.getSelfACI().equals(identifier))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
logger.debug("Found already loaded manager for ACI: {}", identifier);
|
||||
return existing;
|
||||
}
|
||||
// Load by ACI
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManagerByAci(identifier);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.debug("Manager not found by ACI: {}", identifier);
|
||||
} catch (IOException | IllegalArgumentException | AccountCheckException e) {
|
||||
logger.warn("Failed to load new manager by ACI: {}", identifier, e);
|
||||
}
|
||||
} else {
|
||||
// Phone number — check already loaded managers
|
||||
var existing = managers.stream()
|
||||
.filter(m -> m.getSelfNumber().equals(identifier))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
// Load by phone number
|
||||
try {
|
||||
final var newManager = signalAccountFiles.initManagerByNumber(identifier);
|
||||
managers.add(newManager);
|
||||
return newManager;
|
||||
} catch (NotRegisteredException | IOException | IllegalArgumentException | AccountCheckException e) {
|
||||
logger.warn("Failed to load manager by number: {}", identifier, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -51,7 +52,16 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
||||
|
||||
private final Map<Long, Long> recipientsMerged = new HashMap<>();
|
||||
|
||||
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = Collections.synchronizedMap(new HashMap<>());
|
||||
private static final int MAX_RECIPIENT_CACHE_SIZE = 2000;
|
||||
|
||||
private final Map<ServiceId, RecipientWithAddress> recipientAddressCache = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(16, 0.75f, true) {
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<ServiceId, RecipientWithAddress> eldest) {
|
||||
return size() > MAX_RECIPIENT_CACHE_SIZE;
|
||||
}
|
||||
});
|
||||
|
||||
public static void createSql(Connection connection) throws SQLException {
|
||||
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
||||
|
||||
@ -18,7 +18,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -28,8 +28,15 @@ public class SessionStore implements SignalServiceSessionStore {
|
||||
|
||||
private static final String TABLE_SESSION = "session";
|
||||
private static final Logger logger = LoggerFactory.getLogger(SessionStore.class);
|
||||
private static final int MAX_CACHE_SIZE = 1000;
|
||||
|
||||
private final Map<Key, SessionRecord> cachedSessions = new HashMap<>();
|
||||
private final Map<Key, SessionRecord> cachedSessions = new LinkedHashMap<>(16, 0.75f, true) {
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Key, SessionRecord> eldest) {
|
||||
return size() > MAX_CACHE_SIZE;
|
||||
}
|
||||
};
|
||||
|
||||
private final Database database;
|
||||
private final int accountIdType;
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import okio.ByteString;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.recipients.TestRecipientId;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.signal.core.models.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata;
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.Content;
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage;
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage;
|
||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||
import org.whispersystems.signalservice.internal.push.GroupContext;
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class IncomingMessageHandlerTest {
|
||||
|
||||
@Test
|
||||
void invalidEnvelopeContentReportsOutOfBoundsBodyRange() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var bodyRange = new BodyRange.Builder().start(4).length(3).style(BodyRange.Style.BOLD).build();
|
||||
final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build();
|
||||
final var content = new Content.Builder().dataMessage(dataMessage).build();
|
||||
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
|
||||
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
|
||||
"[DataMessage] Body range with out-of-bounds start/length!",
|
||||
new Throwable());
|
||||
|
||||
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
|
||||
metadata,
|
||||
content);
|
||||
|
||||
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
|
||||
assertEquals(sender.toString(), exception.getSender());
|
||||
assertEquals(2, exception.getSenderDevice());
|
||||
assertEquals(5, exception.getBodyLength());
|
||||
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 4, 3, "STYLE_BOLD")),
|
||||
exception.getInvalidBodyRanges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromSyncMessage() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var bodyRange = new BodyRange.Builder().start(-1).length(2).mentionAci(sender.toString()).build();
|
||||
final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build();
|
||||
final var sent = new SyncMessage.Sent.Builder().message(dataMessage).build();
|
||||
final var content = new Content.Builder().syncMessage(new SyncMessage.Builder().sent(sent).build()).build();
|
||||
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
|
||||
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
|
||||
"[DataMessage] Body range with out-of-bounds start/length!",
|
||||
new Throwable());
|
||||
|
||||
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
|
||||
metadata,
|
||||
content);
|
||||
|
||||
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
|
||||
assertEquals(5, exception.getBodyLength());
|
||||
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, -1, 2, "MENTION")),
|
||||
exception.getInvalidBodyRanges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromEditMessage() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var bodyRange = new BodyRange.Builder().start(5).length(1).style(BodyRange.Style.ITALIC).build();
|
||||
final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build();
|
||||
final var editMessage = new EditMessage.Builder().targetSentTimestamp(1L).dataMessage(dataMessage).build();
|
||||
final var content = new Content.Builder().editMessage(editMessage).build();
|
||||
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
|
||||
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
|
||||
"[EditMessage] Body range with out-of-bounds start/length!",
|
||||
new Throwable());
|
||||
|
||||
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
|
||||
metadata,
|
||||
content);
|
||||
|
||||
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
|
||||
assertEquals(5, exception.getBodyLength());
|
||||
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 5, 1, "STYLE_ITALIC")),
|
||||
exception.getInvalidBodyRanges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromQuote() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var bodyRange = new BodyRange.Builder().start(2).length(2).style(BodyRange.Style.MONOSPACE).build();
|
||||
final var quote = new DataMessage.Quote.Builder().text("hey").bodyRanges(List.of(bodyRange)).build();
|
||||
final var dataMessage = new DataMessage.Builder().body("outer body").quote(quote).build();
|
||||
final var content = new Content.Builder().dataMessage(dataMessage).build();
|
||||
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
|
||||
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
|
||||
"[DataMessage] Quote body range with out-of-bounds start/length!",
|
||||
new Throwable());
|
||||
|
||||
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
|
||||
metadata,
|
||||
content);
|
||||
|
||||
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
|
||||
assertEquals(3, exception.getBodyLength());
|
||||
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 2, 2, "STYLE_MONOSPACE")),
|
||||
exception.getInvalidBodyRanges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidSealedSenderCanBeResolvedForBlocking() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var envelope = new SignalServiceEnvelope(new Envelope.Builder()
|
||||
.type(Envelope.Type.UNIDENTIFIED_SENDER)
|
||||
.clientTimestamp(1L)
|
||||
.build(), 2L);
|
||||
final var exception = new InvalidEnvelopeContentException("invalid body range",
|
||||
InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
|
||||
sender.toString(),
|
||||
2,
|
||||
5,
|
||||
List.of(),
|
||||
new Throwable());
|
||||
|
||||
final var source = IncomingMessageHandler.getSenderAddress(envelope, null, exception);
|
||||
|
||||
assertEquals(sender, source.getServiceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidGroupContextIsAvailableForFiltering() {
|
||||
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
|
||||
final var groupId = new byte[16];
|
||||
final var group = new GroupContext.Builder()
|
||||
.id(ByteString.of(groupId))
|
||||
.type(GroupContext.Type.DELIVER)
|
||||
.build();
|
||||
final var bodyRange = new BodyRange.Builder().start(4).length(3).style(BodyRange.Style.BOLD).build();
|
||||
final var dataMessage = new DataMessage.Builder()
|
||||
.body("hello")
|
||||
.bodyRanges(List.of(bodyRange))
|
||||
.group(group)
|
||||
.build();
|
||||
final var content = new Content.Builder().dataMessage(dataMessage).build();
|
||||
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
|
||||
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
|
||||
"[DataMessage] Body range with out-of-bounds start/length!",
|
||||
new Throwable());
|
||||
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
|
||||
metadata,
|
||||
content);
|
||||
|
||||
final var filterInfo = IncomingMessageHandler.getGroupFilterInfo(null, content, exception);
|
||||
|
||||
assertNotNull(filterInfo);
|
||||
assertEquals(GroupId.v1(groupId), filterInfo.groupId());
|
||||
assertTrue(filterInfo.hasAnnouncementContent());
|
||||
|
||||
final var storedGroup = new GroupInfoV1(GroupId.v1(groupId));
|
||||
storedGroup.setBlocked(true);
|
||||
assertTrue(IncomingMessageHandler.isGroupBlocked(storedGroup));
|
||||
assertTrue(IncomingMessageHandler.isNotAllowedToSendToGroup(storedGroup,
|
||||
TestRecipientId.createTestId(1),
|
||||
filterInfo));
|
||||
}
|
||||
}
|
||||
@ -632,6 +632,16 @@ Specify the timestamp of the message to which to react.
|
||||
*--type* TYPE::
|
||||
Specify the receipt type, either `read` (the default) or `viewed`.
|
||||
|
||||
=== sendStory
|
||||
|
||||
Post a file attachment story to your Story, visible to all contacts.
|
||||
|
||||
*-a* ATTACHMENT, *--attachment* ATTACHMENT::
|
||||
Specify the file path to the image or video to post as a story.
|
||||
|
||||
*--no-replies*::
|
||||
Disable replies on this story. By default, replies are allowed.
|
||||
|
||||
=== sendTyping
|
||||
|
||||
Send typing message to trigger a typing indicator for the recipient.
|
||||
|
||||
@ -18,12 +18,13 @@ fi
|
||||
VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1)
|
||||
echo "$VERSION" >dist/VERSION
|
||||
|
||||
# Build jar
|
||||
# Build jar and schemas
|
||||
$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile .
|
||||
git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**'
|
||||
# shellcheck disable=SC2086
|
||||
$ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build
|
||||
mv build/distributions/signal-cli-*.tar.gz dist/
|
||||
mv build/signal-cli-*-json-schemas.tar.gz dist/
|
||||
|
||||
if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then
|
||||
echo -e "\e[33mBuild was performed with overridden Java version $OVERRIDE_JAVA_VERSION, native-image and client will not be built.\e[0m"
|
||||
|
||||
@ -12,7 +12,7 @@ reset_file_dates
|
||||
|
||||
if [ "$1" == "build" ]; then
|
||||
|
||||
./gradlew build \
|
||||
./gradlew build jsonSchemas \
|
||||
--no-daemon \
|
||||
--max-workers=1 \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
@ -20,6 +20,12 @@ if [ "$1" == "build" ]; then
|
||||
-Dorg.gradle.caching=false \
|
||||
-Porg.gradle.java.installations.auto-download=false \
|
||||
-Porg.gradle.java.installations.auto-detect=false
|
||||
|
||||
schemas_tar="build/signal-cli-${VERSION}-json-schemas.tar"
|
||||
reset_file_dates
|
||||
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner -cf "$schemas_tar" -C build/generated/META-INF/schemas .
|
||||
gzip -n -9 "$schemas_tar"
|
||||
|
||||
cd man
|
||||
make install
|
||||
cd ..
|
||||
|
||||
@ -22,6 +22,8 @@ public interface Signal extends DBusInterface {
|
||||
|
||||
String getSelfNumber();
|
||||
|
||||
String getSelfACI();
|
||||
|
||||
void subscribeReceive();
|
||||
|
||||
void unsubscribeReceive();
|
||||
|
||||
@ -184,16 +184,20 @@ public class App {
|
||||
}
|
||||
|
||||
account = getAccountIfOnlyOne(signalAccountFiles);
|
||||
} else if (!Manager.isValidNumber(account, null)) {
|
||||
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
||||
}
|
||||
|
||||
if (command instanceof RegistrationCommand registrationCommand) {
|
||||
if (!Manager.isValidNumber(account, null)) {
|
||||
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
||||
}
|
||||
handleRegistrationCommand(registrationCommand, account, signalAccountFiles, commandHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command instanceof LocalCommand localCommand) {
|
||||
if (!Manager.isValidNumber(account, null) && !Manager.isValidAci(account)) {
|
||||
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
|
||||
}
|
||||
handleLocalCommand(localCommand, account, signalAccountFiles, commandHandler);
|
||||
return;
|
||||
}
|
||||
@ -330,7 +334,11 @@ public class App {
|
||||
) throws CommandException {
|
||||
logger.trace("Loading account file for {}", account);
|
||||
try {
|
||||
return signalAccountFiles.initManager(account);
|
||||
if (Manager.isValidAci(account)) {
|
||||
return signalAccountFiles.initManagerByAci(account);
|
||||
} else {
|
||||
return signalAccountFiles.initManagerByNumber(account);
|
||||
}
|
||||
} catch (NotRegisteredException e) {
|
||||
throw new UserErrorException("User " + account + " is not registered.");
|
||||
} catch (AccountCheckException ace) {
|
||||
|
||||
@ -52,6 +52,7 @@ public class Commands {
|
||||
addCommand(new SendPollTerminateCommand());
|
||||
addCommand(new SendReactionCommand());
|
||||
addCommand(new SendReceiptCommand());
|
||||
addCommand(new SendStoryCommand());
|
||||
addCommand(new SendSyncRequestCommand());
|
||||
addCommand(new SendTypingCommand());
|
||||
addCommand(new SendUnpinMessageCommand());
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import net.sourceforge.argparse4j.impl.Arguments;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
|
||||
import org.asamk.signal.commands.exceptions.CommandException;
|
||||
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
|
||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.output.OutputWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
||||
|
||||
public class SendStoryCommand implements JsonRpcLocalCommand {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "sendStory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachToSubparser(final Subparser subparser) {
|
||||
subparser.help("Post a story to your Story.");
|
||||
subparser.addArgument("-a", "--attachment")
|
||||
.required(true)
|
||||
.help("Specify the file path to the image or video to post as a story.");
|
||||
subparser.addArgument("--no-replies")
|
||||
.action(Arguments.storeTrue())
|
||||
.help("Disable replies on this story.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(
|
||||
final Namespace ns,
|
||||
final Manager m,
|
||||
final OutputWriter outputWriter
|
||||
) throws CommandException {
|
||||
final var attachment = ns.getString("attachment");
|
||||
if (attachment == null || attachment.isEmpty()) {
|
||||
throw new UserErrorException("An attachment is required for sending a story.");
|
||||
}
|
||||
|
||||
final var noReplies = Boolean.TRUE.equals(ns.getBoolean("no-replies"));
|
||||
|
||||
try {
|
||||
final var results = m.sendStory(attachment, !noReplies);
|
||||
outputResult(outputWriter, results);
|
||||
} catch (AttachmentInvalidException | IOException e) {
|
||||
throw new UnexpectedErrorException("Failed to send story: " + e.getMessage() + " (" + e.getClass()
|
||||
.getSimpleName() + ")", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -113,6 +113,11 @@ public class DbusManagerImpl implements Manager {
|
||||
return signal.getSelfNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return signal.getSelfACI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, UserStatus> getUserStatus(final Set<String> numbers) throws IOException {
|
||||
final var numbersList = new ArrayList<>(numbers);
|
||||
@ -542,6 +547,11 @@ public class DbusManagerImpl implements Manager {
|
||||
return new SendMessageResults(timestamp, Map.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendStory(String attachment, boolean allowsReplies) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
|
||||
signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
||||
|
||||
@ -141,6 +141,11 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||
return m.getSelfNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return m.getSelfACI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeReceive() {
|
||||
if (dbusMessageHandler == null) {
|
||||
|
||||
@ -262,6 +262,9 @@ public class HttpServerHandler implements AutoCloseable {
|
||||
} else {
|
||||
final var manager = c.getManager(account);
|
||||
if (manager == null) {
|
||||
// Account not found by the given identifier (number or ACI/UUID)
|
||||
// Log the available accounts to help debug
|
||||
logger.warn("Account not found for identifier: {}", account);
|
||||
return null;
|
||||
}
|
||||
return List.of(manager);
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
package org.asamk.signal.json;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.micronaut.jsonschema.JsonSchema;
|
||||
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonSchema(title = "Error")
|
||||
public record JsonError(String message, String type) {
|
||||
public record JsonError(
|
||||
String message,
|
||||
String type,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL) Details details
|
||||
) {
|
||||
|
||||
public static JsonError from(Throwable exception) {
|
||||
return new JsonError(exception.getMessage(), exception.getClass().getSimpleName());
|
||||
final var details = exception instanceof InvalidEnvelopeContentException e
|
||||
? new Details(e.getCode(), e.getBodyLength(), e.getInvalidBodyRanges())
|
||||
: null;
|
||||
return new JsonError(exception.getMessage(), exception.getClass().getSimpleName(), details);
|
||||
}
|
||||
|
||||
public record Details(
|
||||
String code,
|
||||
Integer bodyLength,
|
||||
List<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ public class Util {
|
||||
for (var param : params) {
|
||||
final var paramParts = param.split("=", 2);
|
||||
var name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
|
||||
var value = paramParts.length == 1 ? null : URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
|
||||
var value = paramParts.length == 1 ? null : URLDecoder.decode(paramParts[1].replace("+", "%2B"), StandardCharsets.UTF_8);
|
||||
map.put(name, value);
|
||||
}
|
||||
return map;
|
||||
|
||||
@ -2949,6 +2949,12 @@
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.json.JsonError$Details",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.json.JsonGroupInfo",
|
||||
"allDeclaredFields": true,
|
||||
@ -3419,6 +3425,12 @@
|
||||
{
|
||||
"type": "org.asamk.signal.logging.LogConfigurator"
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.manager.api.InvalidEnvelopeContentException$InvalidBodyRange",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.manager.api.PhoneNumberSharingMode",
|
||||
"allDeclaredFields": true
|
||||
|
||||
@ -123,6 +123,11 @@ class SseInitialFlushTest {
|
||||
return "+10000000000";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return "00000000-0000-0000-0000-000000000000";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addReceiveHandler(ReceiveMessageHandler handler, boolean isWeakListener) {
|
||||
// no-op
|
||||
@ -349,6 +354,11 @@ class SseInitialFlushTest {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendStory(String attachment, boolean allowsReplies) {
|
||||
return new SendMessageResults(0, Map.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideRecipient(RecipientIdentifier.Single recipient) {
|
||||
}
|
||||
|
||||
50
src/test/java/org/asamk/signal/json/JsonErrorTest.java
Normal file
50
src/test/java/org/asamk/signal/json/JsonErrorTest.java
Normal file
@ -0,0 +1,50 @@
|
||||
package org.asamk.signal.json;
|
||||
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
import org.asamk.signal.util.Util;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JsonErrorTest {
|
||||
|
||||
@Test
|
||||
void invalidEnvelopeContentIncludesStructuredDetails() throws Exception {
|
||||
final var invalidRange = new InvalidEnvelopeContentException.InvalidBodyRange(2, 8, 4, "STYLE_BOLD");
|
||||
final var exception = new InvalidEnvelopeContentException("invalid body range",
|
||||
InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
|
||||
null,
|
||||
3,
|
||||
10,
|
||||
List.of(invalidRange),
|
||||
new Throwable());
|
||||
|
||||
final var error = JsonError.from(exception);
|
||||
|
||||
assertEquals("invalid body range", error.message());
|
||||
assertEquals("InvalidEnvelopeContentException", error.type());
|
||||
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
|
||||
error.details().code());
|
||||
assertEquals(10, error.details().bodyLength());
|
||||
assertEquals(List.of(invalidRange), error.details().invalidBodyRanges());
|
||||
final var json = Util.createJsonObjectMapper().writeValueAsString(error);
|
||||
assertTrue(json.contains("\"code\":\"DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS\""));
|
||||
assertTrue(json.contains("\"start\":8"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryExceptionDoesNotIncludeDetails() throws Exception {
|
||||
final var error = JsonError.from(new IllegalArgumentException("bad argument"));
|
||||
|
||||
assertEquals("bad argument", error.message());
|
||||
assertEquals("IllegalArgumentException", error.type());
|
||||
assertNull(error.details());
|
||||
final var json = Util.createJsonObjectMapper().writeValueAsString(error);
|
||||
assertFalse(json.contains("details"));
|
||||
}
|
||||
}
|
||||
@ -120,6 +120,11 @@ class SubscribeCallEventsTest {
|
||||
return selfNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSelfACI() {
|
||||
return "00000000-0000-0000-0000-000000000000";
|
||||
}
|
||||
|
||||
// --- Stubs for remaining Manager interface methods ---
|
||||
@Override
|
||||
public Map<String, UserStatus> getUserStatus(Set<String> n) {
|
||||
@ -367,6 +372,11 @@ class SubscribeCallEventsTest {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendStory(String attachment, boolean allowsReplies) {
|
||||
return new SendMessageResults(0, Map.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideRecipient(RecipientIdentifier.Single r) {
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user