* Surface retry-after seconds for plain rate-limit failures
libsignal-service's RateLimitException exposes retryAfterMilliseconds
for HTTP 413 responses, but signal-cli only forwarded retry-after for
ProofRequired (428) failures. Clients had no signal for when it was
safe to retry plain rate-limited sends, so every failed retry
potentially extended the server-side window.
SendMessageResult now carries an optional rateLimitRetryAfterSeconds,
populated from the upstream Optional<Long>. JsonSendMessageResult
exposes it for RATE_LIMIT_FAILURE type. Text output includes the
window when known. Aggregate RateLimitErrorException now carries the
real nextAttemptTimestamp (was hardcoded to 0).
Closes#1996.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review: include proof-required retry-after and ceiling-round millis
Codex adversarial review flagged two issues in the phase 1 retry-after
plumbing:
* Aggregate retry-after ignored proof-required failures. Because
isRateLimitFailure is true for proof-required cases but
rateLimitRetryAfterSeconds was only populated from plain 413s, an
all-proof-required batch (or a mixed batch where the proof-required
delay was longer) could flow into outputResult() and produce a
RateLimitException(0), telling callers to retry immediately.
* Millisecond Retry-After values were truncated by integer division,
so 1..999ms became 0 and non-second-aligned values lost up to 999ms.
A retry suggested from the floored value can land before the
server's real deadline and re-trigger the limit.
SendMessageResult.from(...) now populates rateLimitRetryAfterSeconds
from either the proof-required seconds or the plain rate-limit ms
(converted via ceiling division), giving maxRateLimitRetryAfterSeconds
a single source of truth. JsonSendMessageResult.from(...) reads the
unified field. New millisToCeilingSeconds helper plus boundary test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Preserve source compat and document retry-after field change
Add a non-canonical 8-arg SendMessageResult constructor that delegates
to the canonical form with null retry-after. This keeps source
compatibility for any downstream code that constructs the record
directly (tests, mocks) without changing the canonical shape. Records
permit additional constructors alongside the canonical one.
Document the retryAfterSeconds meaning change in the CHANGELOG. The
field was previously populated only for proof-required failures; it
is now populated whenever the server sends a Retry-After header. The
canonical proof-required discriminator is still token != null.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix sender key re-distribution on every group message in daemon mode
sendGroupMessageInternalWithSenderKey() calls sender.send() which handles
distribution and delivery, but never calls markSenderKeySharedWith() on
success. SenderKeySharedStore therefore has no record that the distribution
was sent, causing it to re-distribute to all recipients on every subsequent
sendGroupMessage call.
This results in a fresh unidentified TLS connection being opened for each
group message (~6s delay per send), even for back-to-back sends to the
same group. All send modes are affected: DBus daemon, JSON-RPC socket/http,
and CLI send command all share the same code path.
The fix mirrors the existing pattern in resendMessage() (line 307): after
a successful send, record each successful recipient's address+device in
the sender key shared store.
* Fix sender key re-distribution on every group message
SenderKeySharedStore.markSenderKeysSharedWith() stored the address using
entry.toString() instead of entry.address(). Since SenderKeySharedEntry is
a Java record, toString() returns the full record representation:
SenderKeySharedEntry[address=<uuid>, deviceId=1]
instead of just the UUID. When signal-service-java later calls
getSenderKeySharedWith() and compares the retrieved addresses against the
current group member UUIDs, the comparison always fails — causing the
distribution message to be re-sent to all recipients on every
sendGroupMessage call.
This results in a fresh unidentified TLS connection being opened for each
group message (~6s delay per send), even for immediate consecutive sends
to the same group. All send modes are affected: DBus daemon, JSON-RPC
socket/http, and the CLI send command all share the same code path.
The fix is a one-character change: entry.address() instead of
entry.toString().
* Add voice call API types, protobuf definitions, and build dependencies
Define call method interfaces in Manager, create API records (CallInfo,
CallOffer, TurnServer), and hand-coded protobuf parsers for RingRTC
signaling messages (ConnectionParametersV4, RtpDataMessage).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Implement call signaling state machine and message routing
Add CallSignalingHelper for x25519 key generation and HKDF-based SRTP
key derivation. Add CallManager for tracking active calls, spawning
call tunnel subprocesses, and handling call lifecycle (offer, answer,
ICE candidates, hangup, busy). Wire call message routing in
IncomingMessageHandler and implement Manager call methods in ManagerImpl.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add call state notification mechanism for JSON-RPC clients
Implement CallEventListener callback pattern that fires on every call
state transition (RINGING_INCOMING, RINGING_OUTGOING, CONNECTING,
CONNECTED, ENDED). The JSON-RPC layer auto-subscribes and pushes
callEvent notifications alongside receive notifications.
Changes:
- Manager.java: Add CallEventListener interface and methods
- ManagerImpl.java: Implement add/removeCallEventListener with cleanup
- DbusManagerImpl.java: Add stub implementation (not supported over DBus)
- JsonCallEvent.java: JSON notification record for call events
- SignalJsonRpcDispatcherHandler.java: Auto-subscribe call event listeners
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add JSON-RPC commands for voice call control
Add startCall, acceptCall, hangupCall, rejectCall, and listCalls
commands for the JSON-RPC daemon interface. Register commands and
update GraalVM metadata for native image support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add call tunnel documentation
Add documentation about the architecture, protocol, and implementation of
signal-call-tunnel, the secure tunnel subprocess for voice calling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove unused integration test tag from lib/build.gradle.kts
The excludeTags("integration") block was added but no tests use the
@Tag("integration") annotation. Revert to upstream's simple
useJUnitPlatform() call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Derive install dir from jar location instead of nonexistent property
The signal.cli.install.dir system property was never set by the Gradle
start script or anywhere else. Replace it with code source detection:
resolve the jar's parent directory to find the install root, then look
for bin/signal-call-tunnel relative to that.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove explicit success responses from hangup and reject commands
Successful commands with no additional information should not return
a response, matching the pattern used by other signal-cli commands
like SendSyncRequestCommand and UpdateConfigurationCommand.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use instanceof pattern matching for call ID extraction
Replace explicit null check and Number cast with instanceof pattern
matching in AcceptCallCommand, HangupCallCommand, and
RejectCallCommand.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Guard handleIncoming* methods against missing call event listeners
Skip processing incoming call offers when no call event listeners are
registered, since there is nobody to notify about the call. For hangup
and busy, also guard when there are no listeners AND no active call
(the tunnel may still need cleanup if already spawned).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use Jackson JSON serialization in CallManager
Replace all manual JSON string concatenation with Jackson ObjectNode
construction and ObjectMapper serialization. Use BigInteger for call
IDs to properly represent unsigned 64-bit values in JSON.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add subscribeCallEvents command for opt-in call event notifications
Call events are no longer subscribed by default. JSON-RPC clients must
explicitly call subscribeCallEvents to receive callEvent notifications
and enable incoming call handling. This avoids sending unwanted call
events to clients that don't use voice calling.
Also adds unsubscribeCallEvents for cleanup, idempotent subscription
guard, and updates CALL_TUNNEL.md to document the subscription step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Replace Unix socket with stdin/stdout for tunnel communication
Use the tunnel subprocess' stdin for sending control messages and
stdout for receiving control events, instead of a separate Unix
domain socket. This eliminates:
- Temporary directory creation (/tmp/sc-<random>/)
- Socket path and auth token in config JSON
- Connection retry loop (50x at 200ms)
- Auth message handshake
- Socket cleanup on call end
The tunnel's stderr is captured separately for logging. Config JSON
is written as the first line on stdin, followed by control messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add support for marking audio attachments as voice notes when sending
messages. Voice notes are displayed inline with a play button in Signal
clients, rather than as file attachments.
This addresses a longstanding TODO in AttachmentUtils.java and resolves
the feature request in #1601.
Changes:
- Add 'voiceNote' field to Message record
- Pass voiceNote flag through AttachmentHelper to AttachmentUtils
- Add .withVoiceNote() to SignalServiceAttachmentStream builder
- Add --voice-note CLI argument to SendCommand
- Support voiceNote parameter in JSON-RPC mode
Usage:
signal-cli send -a audio.m4a --voice-note +1234567890
JSON-RPC:
{"method":"send","params":{"attachment":"audio.m4a","voiceNote":true,...}}
Closes#1601
Fix type mismatch in retainAll() calls that cause updateGroup --admin
and --remove-member to silently do nothing.
retainAll() compared Set<RecipientId> against Collection<GroupMemberInfo>,
which always evaluates to false (different types), emptying the set.
Replace group.getMembers() with group.getMemberRecipientIds() in all
three affected locations.
Fixes: updateGroup --admin silently failing to promote members
Fixes: updateGroup --remove-member silently failing to remove members
Co-authored-by: joey <joey@Mac-Studio.local>
* 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.
Implement two new CLI flags to disable downloading avatars and sticker
packs during message reception, following the existing pattern of
--ignore-attachments and --ignore-stories flags.
Changes:
- Add --ignore-avatars and --ignore-stickers flags to ReceiveCommand,
DaemonCommand, and JsonRpcDispatcherCommand
- Extend ReceiveConfig record with ignoreAvatars and ignoreStickers
fields
- Pass ignoreAvatars as explicit boolean parameter to ProfileHelper,
SyncHelper, and GroupHelper methods (per maintainer feedback)
- Gate avatar downloads in ProfileHelper (profile avatars), SyncHelper
(contact avatars), and GroupHelper (group avatars for V1 and V2)
- Gate sticker pack downloads in IncomingMessageHandler for both
direct sticker messages and sync sticker pack operations
- Update handleSignalServiceDataMessage and handleSyncMessage to pass
full ReceiveConfig instead of individual boolean flags
- Update man page (signal-cli.1.adoc) with flag documentation
- Add entries to CHANGELOG.md
When these flags are set, the respective content is not downloaded
during message reception. Metadata (avatar paths, sticker pack IDs)
is still stored, and existing FileNotFoundException handling will
surface if content is later requested but wasn't downloaded.
Fixes#1903Closes#1904