Compare commits

...

12 Commits

Author SHA1 Message Date
Simon Wade
b40bfd5a63
Merge e54df4367fc1ce4150be24af5a062cfe1983d134 into f9cbfa6d6c92291b54a31743a68d913dd819a4cb 2026-02-26 11:17:45 +01:00
AsamK
f9cbfa6d6c Invert urgent boolean in Message to be consistent 2026-02-25 21:43:52 +01:00
Kai Kozlov
d4b3816c5d
Add --no-urgent flag to send command (#1933)
* Add --no-push flag to send command

Expose the server's `urgent` parameter so callers can skip sending a
push notification (FCM/APNs) to the recipient. The message is still
delivered in real-time over WebSocket if the recipient's app is active.

The flag is added to the Message record (following the same pattern as
viewOnce) and threaded through ManagerImpl and SendHelper, keeping the
Manager interface unchanged.

* Rename --no-push flag to --no-urgent

Align with the protocol naming as suggested by the maintainer.
The flag controls the 'urgent' parameter on the server request.
2026-02-25 21:42:51 +01:00
AsamK
4a35d47515 Set explicit console colors for qr code 2026-02-25 21:37:41 +01:00
AsamK
52d4d61e2b Improve aci/pni handling in storage contact sync 2026-02-25 21:36:10 +01:00
AsamK
5bff902394 Configure signal service logger 2026-02-25 21:02:22 +01:00
AsamK
f33eb86335 Fix remote updates of unregistered contacts 2026-02-25 20:23:44 +01:00
AsamK
2ea26b9d1b Load recipient profiles in listContacts command if required 2026-02-25 20:08:23 +01:00
AsamK
10fa3e1619 Load unregistered_timestamp for recipient 2026-02-25 20:08:01 +01:00
AsamK
956e17c81c Improve aci/pni comparison in contact record processor 2026-02-25 19:49:29 +01:00
AsamK
6f749352d8 Split unregistered recipients when loading profile fails with 404 2026-02-25 19:48:58 +01:00
Simon Wade
e54df4367f Add --captcha flag example to registration instructions in README
The existing registration docs mention captcha may be required but don't
show the command syntax. Add an inline example so users can see how to
pass the captcha token without having to navigate to the wiki.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:17:31 +11:00
18 changed files with 205 additions and 67 deletions

View File

@ -79,7 +79,10 @@ of all country codes.)
```
Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha).
In this case, provide the captcha token with the `--captcha` flag:
signal-cli -a ACCOUNT register --captcha CAPTCHA_TOKEN
* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code
to your account

View File

@ -1,10 +1,12 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.internal.LibSignalLogger;
import org.asamk.signal.manager.internal.SignalLogger;
public class ManagerLogger {
public static void initLogger() {
LibSignalLogger.initLogger();
SignalLogger.initLogger();
}
}

View File

@ -12,7 +12,8 @@ public record Message(
Optional<Sticker> sticker,
List<Preview> previews,
Optional<StoryReply> storyReply,
List<TextStyle> textStyles
List<TextStyle> textStyles,
boolean urgent
) {
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}

View File

@ -682,7 +682,7 @@ public class GroupHelper {
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, false, Optional.empty());
context.getSendHelper().sendAsGroupMessage(messageBuilder, groupId, false, Optional.empty(), true);
}
private SendGroupMessageResults updateGroupV2(

View File

@ -84,7 +84,8 @@ public class SendHelper {
public SendMessageResult sendMessage(
final SignalServiceDataMessage.Builder messageBuilder,
final RecipientId recipientId,
Optional<Long> editTargetTimestamp
Optional<Long> editTargetTimestamp,
boolean urgent
) {
var contact = account.getContactStore().getContact(recipientId);
if (contact == null || !contact.isProfileSharingEnabled() || contact.isHidden()) {
@ -102,7 +103,7 @@ public class SendHelper {
}
final var message = messageBuilder.build();
return sendMessage(message, recipientId, editTargetTimestamp);
return sendMessage(message, recipientId, editTargetTimestamp, urgent);
}
/**
@ -113,10 +114,11 @@ public class SendHelper {
final SignalServiceDataMessage.Builder messageBuilder,
final GroupId groupId,
final boolean includeSelf,
final Optional<Long> editTargetTimestamp
final Optional<Long> editTargetTimestamp,
boolean urgent
) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
final var g = getGroupForSending(groupId);
return sendAsGroupMessage(messageBuilder, g, includeSelf, editTargetTimestamp);
return sendAsGroupMessage(messageBuilder, g, includeSelf, editTargetTimestamp, urgent);
}
/**
@ -128,7 +130,7 @@ public class SendHelper {
final Set<RecipientId> recipientIds,
final GroupInfo groupInfo
) throws IOException {
return sendGroupMessage(message, recipientIds, groupInfo, ContentHint.IMPLICIT, Optional.empty());
return sendGroupMessage(message, recipientIds, groupInfo, ContentHint.IMPLICIT, Optional.empty(), true);
}
public SendMessageResult sendReceiptMessage(
@ -311,7 +313,8 @@ public class SendHelper {
final SignalServiceDataMessage.Builder messageBuilder,
final GroupInfo g,
final boolean includeSelf,
final Optional<Long> editTargetTimestamp
final Optional<Long> editTargetTimestamp,
boolean urgent
) throws IOException, GroupSendingNotAllowedException {
GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTimer());
@ -330,7 +333,7 @@ public class SendHelper {
}
}
return sendGroupMessage(message, recipients, g, ContentHint.RESENDABLE, editTargetTimestamp);
return sendGroupMessage(message, recipients, g, ContentHint.RESENDABLE, editTargetTimestamp, urgent);
}
private List<SendMessageResult> sendGroupMessage(
@ -338,13 +341,13 @@ public class SendHelper {
final Set<RecipientId> recipientIds,
final GroupInfo groupInfo,
final ContentHint contentHint,
final Optional<Long> editTargetTimestamp
final Optional<Long> editTargetTimestamp,
boolean urgent
) throws IOException {
final var messageSender = dependencies.getMessageSender();
final var messageSendLogStore = account.getMessageSendLogStore();
final AtomicLong entryId = new AtomicLong(-1);
final var urgent = true;
final PartialSendCompleteListener partialSendCompleteListener = sendResult -> {
logger.trace("Partial message send result: {}", sendResult.isSuccess());
synchronized (entryId) {
@ -712,10 +715,10 @@ public class SendHelper {
private SendMessageResult sendMessage(
SignalServiceDataMessage message,
RecipientId recipientId,
Optional<Long> editTargetTimestamp
Optional<Long> editTargetTimestamp,
boolean urgent
) {
final var messageSendLogStore = account.getMessageSendLogStore();
final var urgent = true;
final var result = handleSendMessage(recipientId,
editTargetTimestamp.isEmpty()
? (messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendDataMessage(

View File

@ -211,18 +211,19 @@ public class StorageHelper {
remoteOnlyRecords.size());
}
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
idDifference.localOnlyIds());
if (updated > 0) {
logger.warn(
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
updated);
}
}
// This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated
// if (!idDifference.localOnlyIds().isEmpty()) {
// final var updated = account.getRecipientStore()
// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
// idDifference.localOnlyIds());
//
// if (updated > 0) {
// logger.warn(
// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
// updated);
// }
// }
//
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()

View File

@ -666,14 +666,15 @@ public class ManagerImpl implements Manager {
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty(), true);
}
private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder,
Set<RecipientIdentifier> recipients,
boolean notifySelf,
Optional<Long> editTargetTimestamp
Optional<Long> editTargetTimestamp,
boolean urgent
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = getNextMessageTimestamp();
@ -685,14 +686,14 @@ public class ManagerImpl implements Manager {
)) {
final var result = notifySelf
? context.getSendHelper()
.sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp)
.sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp, urgent)
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} else if (recipient instanceof RecipientIdentifier.Single single) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
final var result = context.getSendHelper()
.sendMessage(messageBuilder, recipientId, editTargetTimestamp);
.sendMessage(messageBuilder, recipientId, editTargetTimestamp, urgent);
results.put(recipient, List.of(toSendMessageResult(result)));
} catch (UnregisteredRecipientException e) {
results.put(recipient,
@ -700,7 +701,7 @@ public class ManagerImpl implements Manager {
}
} else if (recipient instanceof RecipientIdentifier.Group group) {
final var result = context.getSendHelper()
.sendAsGroupMessage(messageBuilder, group.groupId(), notifySelf, editTargetTimestamp);
.sendAsGroupMessage(messageBuilder, group.groupId(), notifySelf, editTargetTimestamp, urgent);
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
}
}
@ -799,7 +800,7 @@ public class ManagerImpl implements Manager {
}
final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message);
return sendMessage(messageBuilder, recipients, notifySelf);
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty(), message.urgent());
}
@Override
@ -810,7 +811,7 @@ public class ManagerImpl implements Manager {
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message);
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp));
return sendMessage(messageBuilder, recipients, false, Optional.of(editTargetTimestamp), message.urgent());
}
private void applyMessage(
@ -1479,7 +1480,21 @@ public class ManagerImpl implements Manager {
return List.of();
}
// refresh profiles of explicitly given recipients
context.getProfileHelper().refreshRecipientProfiles(recipientIds);
if (recipientIds.isEmpty()) {
final var rIds = account.getRecipientStore()
.getRecipients(onlyContacts, blocked, recipientIds, name)
.stream()
.filter(r -> r.isRegistered())
.map(r -> r.getRecipientId())
.toList();
try {
context.getProfileHelper().getRecipientProfiles(rIds);
} catch (Exception e) {
logger.warn("Failed to refresh profiles for recipients", e);
}
} else {
context.getProfileHelper().refreshRecipientProfiles(recipientIds);
}
return account.getRecipientStore()
.getRecipients(onlyContacts, blocked, recipientIds, name)
.stream()

View File

@ -0,0 +1,46 @@
package org.asamk.signal.manager.internal;
import org.signal.core.util.logging.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SignalLogger extends Log.Logger {
private static final Logger logger = LoggerFactory.getLogger("LibSignalService");
public static void initLogger() {
Log.initialize(() -> true, new SignalLogger());
}
private SignalLogger() {
}
@Override
public void v(final String tag, final String message, final Throwable throwable, final boolean b) {
logger.trace("[{}]: {}", tag, message, throwable);
}
@Override
public void d(final String tag, final String message, final Throwable throwable, final boolean b) {
logger.debug("[{}]: {}", tag, message, throwable);
}
@Override
public void i(final String tag, final String message, final Throwable throwable, final boolean b) {
logger.info("[{}]: {}", tag, message, throwable);
}
@Override
public void w(final String tag, final String message, final Throwable throwable, final boolean b) {
logger.warn("[{}]: {}", tag, message, throwable);
}
@Override
public void e(final String tag, final String message, final Throwable throwable, final boolean b) {
logger.error("[{}]: {}", tag, message, throwable);
}
@Override
public void flush() {
}
}

View File

@ -99,6 +99,7 @@ public class LegacyRecipientStore2 {
expiringProfileKeyCredential,
profile,
null,
null,
null);
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));

View File

@ -23,6 +23,8 @@ public class Recipient {
private final Boolean discoverable;
private final Long unregisteredTimestamp;
private final byte[] storageRecord;
public Recipient(
@ -33,6 +35,7 @@ public class Recipient {
final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile,
final Boolean discoverable,
final Long unregisteredTimestamp,
final byte[] storageRecord
) {
this.recipientId = recipientId;
@ -42,6 +45,7 @@ public class Recipient {
this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile;
this.discoverable = discoverable;
this.unregisteredTimestamp = unregisteredTimestamp;
this.storageRecord = storageRecord;
}
@ -53,6 +57,7 @@ public class Recipient {
expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
profile = builder.profile;
discoverable = builder.discoverable;
unregisteredTimestamp = builder.unregisteredTimestamp;
storageRecord = builder.storageRecord;
}
@ -100,6 +105,14 @@ public class Recipient {
return discoverable;
}
public Long getUnregisteredTimestamp() {
return unregisteredTimestamp;
}
public boolean isRegistered() {
return unregisteredTimestamp == null;
}
public byte[] getStorageRecord() {
return storageRecord;
}
@ -131,6 +144,7 @@ public class Recipient {
private ExpiringProfileKeyCredential expiringProfileKeyCredential;
private Profile profile;
private Boolean discoverable;
private Long unregisteredTimestamp;
private byte[] storageRecord;
private Builder() {
@ -171,6 +185,11 @@ public class Recipient {
return this;
}
public Builder withUnregisteredTimestamp(final Long val) {
unregisteredTimestamp = val;
return this;
}
public Builder withStorageRecord(final byte[] val) {
storageRecord = val;
return this;

View File

@ -981,7 +981,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
if (registered) {
markRegistered(connection, recipientId);
} else {
markUnregistered(connection, recipientId);
markUnregisteredAndSplitIfNecessary(connection, recipientId);
}
connection.commit();
} catch (SQLException e) {
@ -1549,6 +1549,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
getExpiringProfileKeyCredentialFromResultSet(resultSet),
getProfileFromResultSet(resultSet),
getDiscoverableFromResultSet(resultSet),
getUnregisteredTimestampFromResultSet(resultSet),
getStorageRecordFromResultSet(resultSet));
}
@ -1580,6 +1581,14 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
return discoverable;
}
private static Long getUnregisteredTimestampFromResultSet(final ResultSet resultSet) throws SQLException {
final var discoverable = resultSet.getLong("unregistered_timestamp");
if (resultSet.wasNull()) {
return null;
}
return discoverable;
}
private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
final var profileCapabilities = resultSet.getString("profile_capabilities");
final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");

View File

@ -31,6 +31,7 @@ import java.util.regex.Pattern;
import okio.ByteString;
import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
import static org.asamk.signal.manager.util.Utils.firstNonNull;
import static org.asamk.signal.manager.util.Utils.nullIfEmpty;
public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
@ -129,27 +130,32 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
}
if ((!local.aci.isEmpty() || local.aciBinary.size() > 0)
final var localAci = ACI.parseOrNull(local.aci, local.aciBinary);
final var localPni = PNI.parseOrNull(local.pni, local.pniBinary);
final var remoteAci = ACI.parseOrNull(remote.aci, remote.aciBinary);
final var remotePni = PNI.parseOrNull(remote.pni, remote.pniBinary);
if (localAci != null
&& local.identityKey.size() > 0
&& remote.identityKey.size() > 0
&& !local.identityKey.equals(remote.identityKey)) {
logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
local.aci);
localAci);
final var address = getRecipientAddress(local);
jobExecutor.enqueueJob(new DownloadProfileJob(address));
}
String pni;
PNI pni;
String e164;
if (account.isPrimaryDevice()) {
final var e164sMatchButPnisDont = !local.e164.isEmpty()
&& local.e164.equals(remote.e164)
&& !local.pni.isEmpty()
&& !remote.pni.isEmpty()
&& !local.pni.equals(remote.pni);
&& localPni != null
&& remotePni != null
&& !localPni.equals(remotePni);
final var pnisMatchButE164sDont = !local.pni.isEmpty()
&& local.pni.equals(remote.pni)
final var pnisMatchButE164sDont = localPni != null
&& localPni.equals(remotePni)
&& !local.e164.isEmpty()
&& !remote.e164.isEmpty()
&& !local.e164.equals(remote.e164);
@ -161,14 +167,14 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
}
jobExecutor.enqueueJob(new RefreshRecipientsJob());
pni = local.pni;
pni = localPni;
e164 = local.e164;
} else {
pni = firstNonEmpty(remote.pni, local.pni);
pni = firstNonNull(remotePni, localPni);
e164 = firstNonEmpty(remote.e164, local.e164);
}
} else {
pni = firstNonEmpty(remote.pni, local.pni);
pni = firstNonNull(remotePni, localPni);
e164 = firstNonEmpty(remote.e164, local.e164);
}
@ -177,11 +183,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
? ByteString.EMPTY
: remote.profileKey;
final var mergedBuilder = remote.newBuilder()
.aci(local.aci.isEmpty() ? remote.aci : local.aci)
.aciBinary(firstNonEmpty(local.aciBinary, remote.aciBinary))
.e164(e164)
.pni(pni)
.pniBinary(pni.isEmpty() ? ByteString.EMPTY : PNI.parseOrThrow(pni).toByteStringWithoutPrefix())
.givenName(profileGivenName)
.familyName(profileFamilyName)
.systemGivenName(account.isPrimaryDevice() ? local.systemGivenName : remote.systemGivenName)
@ -203,6 +205,28 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
.nickname(remote.nickname)
.note(remote.note)
.avatarColor(remote.avatarColor);
if (remote.aci.isEmpty() && remote.aciBinary.size() == 0) {
mergedBuilder.aci(localAci == null ? remote.aci : localAci.toString())
.aciBinary(localAci == null ? remote.aciBinary : localAci.toByteString());
} else {
if (!remote.aci.isEmpty()) {
mergedBuilder.aci(localAci == null ? remote.aci : localAci.toString());
}
if (remote.aciBinary.size() > 0) {
mergedBuilder.aciBinary(localAci == null ? remote.aciBinary : localAci.toByteString());
}
}
if (remote.pni.isEmpty() && remote.pniBinary.size() == 0) {
mergedBuilder.pni(pni == null ? "" : pni.toStringWithoutPrefix())
.pniBinary(pni == null ? ByteString.EMPTY : pni.toByteStringWithoutPrefix());
} else {
if (!remote.pni.isEmpty()) {
mergedBuilder.pni(pni == null ? "" : pni.toStringWithoutPrefix());
}
if (remote.pniBinary.size() > 0) {
mergedBuilder.pniBinary(pni == null ? ByteString.EMPTY : pni.toByteStringWithoutPrefix());
}
}
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
@ -337,15 +361,16 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
public int compare(SignalContactRecord lhsRecord, SignalContactRecord rhsRecord) {
final var lhs = lhsRecord.getProto();
final var rhs = rhsRecord.getProto();
final var lhsAci = ACI.parseOrNull(lhs.aci, lhs.aciBinary);
final var rhsAci = ACI.parseOrNull(rhs.aci, rhs.aciBinary);
final var lhsPni = PNI.parseOrNull(lhs.pni, lhs.pniBinary);
final var rhsPni = PNI.parseOrNull(rhs.pni, rhs.pniBinary);
if ((
(!lhs.aci.isEmpty() && Objects.equals(lhs.aci, rhs.aci)) || (
lhs.aciBinary.size() != 0 && Objects.equals(lhs.aciBinary, rhs.aciBinary)
)
(lhsAci != null && Objects.equals(lhsAci, rhsAci))
) || (
!lhs.e164.isEmpty() && Objects.equals(lhs.e164, rhs.e164)
) || (
(!lhs.pni.isEmpty() && Objects.equals(lhs.pni, rhs.pni) && lhs.pniBinary == rhs.pniBinary)
|| (lhs.pniBinary.size() != 0 && Objects.equals(lhs.pniBinary, rhs.pniBinary))
(lhsPni != null && Objects.equals(lhsPni, rhsPni))
)) {
return 0;
} else {

View File

@ -33,7 +33,7 @@ import static org.signal.core.util.StringExtensionsKt.emptyIfNull;
public final class StorageSyncModels {
private final static boolean useBinaryId = false;
private final static boolean useBinaryId = true;
private final static boolean useStringId = true;
private StorageSyncModels() {

View File

@ -186,8 +186,8 @@ public final class StorageSyncValidations {
if (insert.getProto().contact != null) {
final var contact = insert.getProto().contact;
final var aci = ACI.parseOrNull(contact.aci);
final var pni = PNI.parseOrNull(contact.pni);
final var aci = ACI.parseOrNull(contact.aci, contact.aciBinary);
final var pni = PNI.parseOrNull(contact.pni, contact.pniBinary);
final var number = contact.e164.isEmpty() ? null : contact.e164;
final var username = contact.username.isEmpty() ? null : contact.username;
final var address = new RecipientAddress(aci, pni, number, username);

View File

@ -52,7 +52,7 @@ public class LinkCommand implements ProvisioningCommand {
try {
final URI deviceLinkUri = m.getDeviceLinkUri();
if (System.console() != null) {
printQrCode(writer, deviceLinkUri);
printQrCode(writer, deviceLinkUri.toString());
}
writer.println("{}", deviceLinkUri);
var number = m.finishDeviceLink(deviceName);
@ -70,12 +70,14 @@ public class LinkCommand implements ProvisioningCommand {
}
}
private void printQrCode(final PlainTextWriter writer, final URI deviceLinkUri) {
private void printQrCode(final PlainTextWriter writer, final String contents) {
try {
var bitMatrix = new QRCodeWriter().encode(deviceLinkUri.toString(), BarcodeFormat.QR_CODE, 0, 0);
var bitMatrix = new QRCodeWriter().encode(contents, BarcodeFormat.QR_CODE, 0, 0);
writer.println("\033[37;40m");
for (int y = 0; y < bitMatrix.getHeight(); y += 2) {
writer.println(formatQRCodeLinePair(bitMatrix, y));
}
writer.println("\033[39;49m");
} catch (WriterException e) {
logger.error("Failed to generate QR code", e);
}
@ -86,7 +88,7 @@ public class LinkCommand implements ProvisioningCommand {
for (int x = 0; x < bitMatrix.getWidth(); x++) {
boolean upper = bitMatrix.get(x, y);
boolean lower = y + 1 < bitMatrix.getHeight() && bitMatrix.get(x, y + 1);
line.append((upper && lower) ? "█" : (upper ? "▀" : (lower ? "▄" : " ")));
line.append((upper && lower) ? " " : (upper ? "▄" : (lower ? "▀" : "█")));
}
return line.toString();
}

View File

@ -105,6 +105,10 @@ public class SendCommand implements JsonRpcLocalCommand {
subparser.addArgument("--edit-timestamp")
.type(long.class)
.help("Specify the timestamp of a previous message with the recipient or group to send an edited message.");
subparser.addArgument("--no-urgent")
.action(Arguments.storeTrue())
.help("Send the message without the urgent flag, so no push notification is triggered for the recipient. "
+ "The message will still be delivered in real-time if the recipient's app is active.");
}
@Override
@ -115,6 +119,7 @@ public class SendCommand implements JsonRpcLocalCommand {
) throws CommandException {
final var notifySelf = Boolean.TRUE.equals(ns.getBoolean("notify-self"));
final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self"));
final var noUrgent = Boolean.TRUE.equals(ns.getBoolean("no-urgent"));
final var recipientStrings = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
final var usernameStrings = ns.<String>getList("username");
@ -247,7 +252,8 @@ public class SendCommand implements JsonRpcLocalCommand {
Optional.ofNullable(sticker),
previews,
Optional.ofNullable((storyReply)),
textStyles);
textStyles,
!noUrgent);
var results = editTimestamp != null
? m.sendEditMessage(message, recipientIdentifiers, editTimestamp)
: m.sendMessage(message, recipientIdentifiers, notifySelf);

View File

@ -242,7 +242,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
Optional.empty(),
List.of(),
Optional.empty(),
List.of());
List.of(),
true);
final var recipientIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet());
@ -407,7 +408,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
Optional.empty(),
List.of(),
Optional.empty(),
List.of());
List.of(),
true);
final var results = m.sendMessage(message, Set.of(RecipientIdentifier.NoteToSelf.INSTANCE), false);
checkSendMessageResults(results);
return results.timestamp();
@ -453,7 +455,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
Optional.empty(),
List.of(),
Optional.empty(),
List.of());
List.of(),
true);
var results = m.sendMessage(message, Set.of(getGroupRecipientIdentifier(groupId)), false);
checkSendMessageResults(results);
return results.timestamp();

View File

@ -64,7 +64,9 @@ public class LogConfigurator extends ContextAwareBase implements Configurator {
consoleAppender.addFilter(new Filter<>() {
@Override
public FilterReply decide(final ILoggingEvent event) {
return !"LibSignal".equals(event.getLoggerName()) && (
return !"LibSignal".equals(event.getLoggerName())
&& !"LibSignalService".equals(event.getLoggerName())
&& (
event.getLevel().isGreaterOrEqual(Level.WARN) || (
event.getLevel().isGreaterOrEqual(Level.INFO) && event.getLoggerName()
.startsWith("org.asamk")