* 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().
The unauthenticated (sealed sender) socket had no keep-alive token
registered, causing SignalWebSocket's DelayedDisconnectThread to tear
down the connection ~10s after each send. Every subsequent group message
then had to re-establish a fresh TLS connection (~6s delay).
The authenticated socket avoids this by registering a "receive" keep-alive
token for the lifetime of the receive loop. Apply the same pattern to the
unauthenticated socket: register the token alongside the authenticated one
and remove it in the same finally block.
This keeps the unidentified connection alive in daemon mode, matching the
behaviour of Signal mobile clients.
* 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>