mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-04-12 07:20:42 +00:00
Some call refactoring
This commit is contained in:
parent
0a777ea7df
commit
7a8a34f45e
@ -61,13 +61,13 @@ The first line written to the tunnel's stdin:
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
|
||||
| `is_outgoing` | boolean | Whether this is an outgoing call |
|
||||
| `local_device_id` | integer | Signal device ID |
|
||||
| `input_device_name` | string (optional) | Requested input audio device name |
|
||||
| `output_device_name` | string (optional) | Requested output audio device name |
|
||||
| Field | Type | Description |
|
||||
|----------------------|-------------------------|-----------------------------------------------|
|
||||
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
|
||||
| `is_outgoing` | boolean | Whether this is an outgoing call |
|
||||
| `local_device_id` | integer | Signal device ID |
|
||||
| `input_device_name` | string (optional) | Requested input audio device name |
|
||||
| `output_device_name` | string (optional) | Requested output audio device name |
|
||||
|
||||
If `input_device_name` or `output_device_name` are omitted, the tunnel
|
||||
chooses default names. On Linux, these are per-call unique names (e.g.,
|
||||
@ -84,33 +84,39 @@ lines are control messages.
|
||||
|
||||
### signal-cli -> Tunnel (stdin)
|
||||
|
||||
| Type | When | Fields |
|
||||
|------|------|--------|
|
||||
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
|
||||
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
|
||||
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
|
||||
| `accept` | User accepts incoming call | *(none)* |
|
||||
| `hangup` | End the call | *(none)* |
|
||||
| Type | When | Fields |
|
||||
|----------------------|----------------------------|---------------------------------------------------------------------------------------------------|
|
||||
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
|
||||
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
|
||||
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
|
||||
| `accept` | User accepts incoming call | *(none)* |
|
||||
| `hangup` | End the call | *(none)* |
|
||||
|
||||
### Tunnel -> signal-cli (stdout)
|
||||
|
||||
| Type | When | Fields |
|
||||
|------|------|--------|
|
||||
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
|
||||
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
|
||||
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
|
||||
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
|
||||
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
|
||||
| `sendBusy` | Line is busy | `callId` |
|
||||
| `stateChange` | Call state transition | `state`, `reason` (optional) |
|
||||
| `error` | Something went wrong | `message` |
|
||||
| Type | When | Fields |
|
||||
|---------------|---------------------------------------------|------------------------------------------------------|
|
||||
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
|
||||
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
|
||||
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
|
||||
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
|
||||
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
|
||||
| `sendBusy` | Line is busy | `callId` |
|
||||
| `stateChange` | Call state transition | `state`, `reason` (optional) |
|
||||
| `error` | Something went wrong | `message` |
|
||||
|
||||
Opaque blobs and identity keys are base64-encoded. ICE servers use the format:
|
||||
|
||||
```json
|
||||
{"urls":["turn:example.com"],"username":"u","password":"p"}
|
||||
{
|
||||
"urls": [
|
||||
"turn:example.com"
|
||||
],
|
||||
"username": "u",
|
||||
"password": "p"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@ -191,7 +197,6 @@ signal-cli signal-call-tunnel Remote Phone
|
||||
### JSON-RPC client perspective
|
||||
|
||||
An external application (bot, UI, test script) interacts via JSON-RPC only.
|
||||
It never touches the control socket directly.
|
||||
|
||||
**Important:** Call event notifications are not sent by default. Clients must
|
||||
call `subscribeCallEvents` before initiating or receiving calls. Without this,
|
||||
|
||||
@ -4,6 +4,8 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.CallOffer;
|
||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.Configuration;
|
||||
@ -37,11 +39,13 @@ import org.asamk.signal.manager.api.ReceiveConfig;
|
||||
import org.asamk.signal.manager.api.Recipient;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||
import org.asamk.signal.manager.api.SendMessageResult;
|
||||
import org.asamk.signal.manager.api.SendMessageResults;
|
||||
import org.asamk.signal.manager.api.StickerPack;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.TypingAction;
|
||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.api.UpdateGroup;
|
||||
@ -64,10 +68,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.CallOffer;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
|
||||
public interface Manager extends Closeable {
|
||||
|
||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
||||
@ -425,17 +425,32 @@ public interface Manager extends Closeable {
|
||||
|
||||
void hangupCall(long callId) throws IOException;
|
||||
|
||||
void rejectCall(long callId) throws IOException;
|
||||
SendMessageResult rejectCall(long callId) throws IOException;
|
||||
|
||||
List<CallInfo> listActiveCalls();
|
||||
|
||||
void sendCallOffer(RecipientIdentifier.Single recipient, CallOffer offer) throws IOException, UnregisteredRecipientException;
|
||||
void sendCallOffer(
|
||||
RecipientIdentifier.Single recipient,
|
||||
CallOffer offer
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
void sendCallAnswer(RecipientIdentifier.Single recipient, long callId, byte[] answerOpaque) throws IOException, UnregisteredRecipientException;
|
||||
void sendCallAnswer(
|
||||
RecipientIdentifier.Single recipient,
|
||||
long callId,
|
||||
byte[] answerOpaque
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
void sendIceUpdate(RecipientIdentifier.Single recipient, long callId, List<byte[]> iceCandidates) throws IOException, UnregisteredRecipientException;
|
||||
void sendIceUpdate(
|
||||
RecipientIdentifier.Single recipient,
|
||||
long callId,
|
||||
List<byte[]> iceCandidates
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
void sendHangup(RecipientIdentifier.Single recipient, long callId, MessageEnvelope.Call.Hangup.Type type) throws IOException, UnregisteredRecipientException;
|
||||
void sendHangup(
|
||||
RecipientIdentifier.Single recipient,
|
||||
long callId,
|
||||
MessageEnvelope.Call.Hangup.Type type
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
|
||||
@ -1,30 +1,40 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -32,6 +42,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
/**
|
||||
* Manages active voice calls: tracks state, spawns/monitors the signal-call-tunnel
|
||||
@ -69,7 +83,7 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
|
||||
private void fireCallEvent(CallState state, String reason) {
|
||||
var callInfo = state.toCallInfo();
|
||||
var callInfo = state.toCallInfo(account.getRecipientAddressResolver());
|
||||
for (var listener : callEventListeners) {
|
||||
try {
|
||||
listener.handleCallEvent(callInfo, reason);
|
||||
@ -80,22 +94,16 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
|
||||
public CallInfo startOutgoingCall(
|
||||
final RecipientIdentifier.Single recipient
|
||||
) throws IOException, UnregisteredRecipientException {
|
||||
final RecipientId recipientId
|
||||
) throws IOException {
|
||||
var callId = generateCallId();
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
var recipientAddress = context.getRecipientHelper()
|
||||
.resolveSignalServiceAddress(recipientId)
|
||||
.getServiceId();
|
||||
var recipientApiAddress = account.getRecipientAddressResolver()
|
||||
.resolveRecipientAddress(recipientId)
|
||||
.toApiRecipientAddress();
|
||||
var recipientAddress = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||
|
||||
var state = new CallState(callId,
|
||||
CallInfo.State.RINGING_OUTGOING,
|
||||
recipientApiAddress,
|
||||
recipient,
|
||||
true);
|
||||
var state = new CallState(callId, CallInfo.State.RINGING_OUTGOING, recipientId, null, true);
|
||||
logger.debug("Starting outgoing call {} to {} (recipientId: {})",
|
||||
callIdUnsigned(callId),
|
||||
recipientAddress,
|
||||
recipientId);
|
||||
activeCalls.put(callId, state);
|
||||
fireCallEvent(state, null);
|
||||
|
||||
@ -108,7 +116,7 @@ public class CallManager implements AutoCloseable {
|
||||
// Send createOutgoingCall + proceed via control channel
|
||||
var createMsg = mapper.createObjectNode();
|
||||
createMsg.put("type", "createOutgoingCall");
|
||||
createMsg.put("callId", callIdUnsigned(callId));
|
||||
createMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||
createMsg.put("peerId", recipientAddress.toString());
|
||||
sendControlMessage(state, writeJson(createMsg));
|
||||
sendProceed(state, callId, turnServers);
|
||||
@ -116,17 +124,18 @@ public class CallManager implements AutoCloseable {
|
||||
// Schedule ring timeout
|
||||
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
|
||||
logger.info("Started outgoing call {} to {}", callId, recipient);
|
||||
return state.toCallInfo();
|
||||
logger.debug("Started outgoing call {} to {}", callIdUnsigned(callId), recipientAddress);
|
||||
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||
}
|
||||
|
||||
public CallInfo acceptIncomingCall(final long callId) throws IOException {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
throw new IOException("No active call with id " + callId);
|
||||
}
|
||||
final var state = getActiveCall(callId);
|
||||
if (state.state != CallInfo.State.RINGING_INCOMING) {
|
||||
throw new IOException("Call " + callId + " is not in RINGING_INCOMING state (current: " + state.state + ")");
|
||||
throw new IOException("Call "
|
||||
+ callId
|
||||
+ " is not in RINGING_INCOMING state (current: "
|
||||
+ state.state
|
||||
+ ")");
|
||||
}
|
||||
|
||||
// Defer the accept until the tunnel reports Ringing state.
|
||||
@ -139,46 +148,34 @@ public class CallManager implements AutoCloseable {
|
||||
state.state = CallInfo.State.CONNECTING;
|
||||
fireCallEvent(state, null);
|
||||
|
||||
logger.info("Accepted incoming call {}", callId);
|
||||
return state.toCallInfo();
|
||||
logger.debug("Accepted incoming call {}", callIdUnsigned(callId));
|
||||
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||
}
|
||||
|
||||
public void hangupCall(final long callId) throws IOException {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
throw new IOException("No active call with id " + callId);
|
||||
}
|
||||
getActiveCall(callId);
|
||||
endCall(callId, "local_hangup");
|
||||
}
|
||||
|
||||
public void rejectCall(final long callId) throws IOException {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
throw new IOException("No active call with id " + callId);
|
||||
}
|
||||
public SendMessageResult rejectCall(final long callId) throws IOException {
|
||||
final var callState = getActiveCall(callId);
|
||||
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var busyMessage = new org.whispersystems.signalservice.api.messages.calls.BusyMessage(callId);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forBusy(
|
||||
busyMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send busy message for call {}", callId, e);
|
||||
}
|
||||
final var result = sendBusyMessage(callState.callId, callState.recipientId, callState.deviceId);
|
||||
|
||||
endCall(callId, "rejected");
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<CallInfo> listActiveCalls() {
|
||||
return activeCalls.values().stream().map(CallState::toCallInfo).toList();
|
||||
return activeCalls.values()
|
||||
.stream()
|
||||
.map((CallState callState) -> callState.toCallInfo(account.getRecipientAddressResolver()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<TurnServer> getTurnServers() throws IOException {
|
||||
try {
|
||||
var result = dependencies.getCallingApi().getTurnServerInfo();
|
||||
var turnServerList = result.successOrThrow();
|
||||
var turnServerList = handleResponseException(dependencies.getCallingApi().getTurnServerInfo());
|
||||
return turnServerList.stream()
|
||||
.map(info -> new TurnServer(info.getUsername(), info.getPassword(), info.getUrls()))
|
||||
.toList();
|
||||
@ -191,47 +188,34 @@ public class CallManager implements AutoCloseable {
|
||||
// --- Incoming call message handling ---
|
||||
|
||||
public void handleIncomingOffer(
|
||||
final org.asamk.signal.manager.storage.recipients.RecipientId senderId,
|
||||
final RecipientId recipientId,
|
||||
final int deviceId,
|
||||
final long callId,
|
||||
final MessageEnvelope.Call.Offer.Type type,
|
||||
final byte[] opaque
|
||||
) {
|
||||
if (callEventListeners.isEmpty()) {
|
||||
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered", callId);
|
||||
try {
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(senderId);
|
||||
var busyMessage = new org.whispersystems.signalservice.api.messages.calls.BusyMessage(callId);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forBusy(
|
||||
busyMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send busy for unhandled call {}", callId, e);
|
||||
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
||||
callIdUnsigned(callId));
|
||||
|
||||
final var result = sendBusyMessage(callId, recipientId, deviceId);
|
||||
if (!result.isSuccess()) {
|
||||
logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var senderAddress = account.getRecipientAddressResolver()
|
||||
.resolveRecipientAddress(senderId)
|
||||
.resolveRecipientAddress(recipientId)
|
||||
.toApiRecipientAddress();
|
||||
|
||||
RecipientIdentifier.Single senderIdentifier;
|
||||
if (senderAddress.number().isPresent()) {
|
||||
senderIdentifier = new RecipientIdentifier.Number(senderAddress.number().get());
|
||||
} else if (senderAddress.uuid().isPresent()) {
|
||||
senderIdentifier = new RecipientIdentifier.Uuid(senderAddress.uuid().get());
|
||||
} else {
|
||||
logger.warn("Cannot identify sender for call {}", callId);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Incoming offer opaque ({} bytes)", opaque == null ? 0 : opaque.length);
|
||||
|
||||
var state = new CallState(callId,
|
||||
CallInfo.State.RINGING_INCOMING,
|
||||
var state = new CallState(callId, CallInfo.State.RINGING_INCOMING, recipientId, deviceId, false);
|
||||
logger.debug("Starting incoming call {} from {} (recipientId: {})",
|
||||
callIdUnsigned(callId),
|
||||
senderAddress,
|
||||
senderIdentifier,
|
||||
false);
|
||||
state.rawOfferOpaque = opaque;
|
||||
recipientId);
|
||||
activeCalls.put(callId, state);
|
||||
|
||||
// Spawn call tunnel binary immediately
|
||||
@ -239,7 +223,7 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
// Get identity keys for the receivedOffer message
|
||||
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey().serialize());
|
||||
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||
|
||||
// Fetch TURN servers
|
||||
@ -247,16 +231,16 @@ public class CallManager implements AutoCloseable {
|
||||
try {
|
||||
turnServers = getTurnServers();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to get TURN servers for incoming call {}", callId, e);
|
||||
logger.warn("Failed to get TURN servers for incoming call {}", callIdUnsigned(callId), e);
|
||||
turnServers = List.of();
|
||||
}
|
||||
|
||||
// Send receivedOffer to subprocess
|
||||
var offerMsg = mapper.createObjectNode();
|
||||
offerMsg.put("type", "receivedOffer");
|
||||
offerMsg.put("callId", callIdUnsigned(callId));
|
||||
offerMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||
offerMsg.put("peerId", senderAddress.toString());
|
||||
offerMsg.put("senderDeviceId", 1);
|
||||
offerMsg.put("senderDeviceId", deviceId);
|
||||
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
offerMsg.put("age", 0);
|
||||
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||
@ -271,40 +255,41 @@ public class CallManager implements AutoCloseable {
|
||||
// Schedule ring timeout
|
||||
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
|
||||
logger.info("Incoming call {} from {}", callId, senderAddress);
|
||||
logger.debug("Incoming call {} from {}", callIdUnsigned(callId), senderAddress);
|
||||
}
|
||||
|
||||
public void handleIncomingAnswer(final long callId, final byte[] opaque) {
|
||||
public void handleIncomingAnswer(final long callId, final int deviceId, final byte[] opaque) {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
logger.warn("Received answer for unknown call {}", callId);
|
||||
logger.warn("Received answer for unknown call {}", callIdUnsigned(callId));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get identity keys
|
||||
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey().serialize());
|
||||
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||
|
||||
// Forward raw opaque to subprocess
|
||||
var answerMsg = mapper.createObjectNode();
|
||||
answerMsg.put("type", "receivedAnswer");
|
||||
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
answerMsg.put("senderDeviceId", 1);
|
||||
answerMsg.put("senderDeviceId", deviceId);
|
||||
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||
sendControlMessage(state, writeJson(answerMsg));
|
||||
|
||||
state.deviceId = deviceId;
|
||||
state.state = CallInfo.State.CONNECTING;
|
||||
fireCallEvent(state, null);
|
||||
|
||||
logger.info("Received answer for call {}", callId);
|
||||
logger.debug("Received answer for call {}", callIdUnsigned(callId));
|
||||
}
|
||||
|
||||
public void handleIncomingIceCandidate(final long callId, final byte[] opaque) {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
logger.debug("Received ICE candidate for unknown call {}", callId);
|
||||
logger.debug("Received ICE candidate for unknown call {}", callIdUnsigned(callId));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -314,7 +299,7 @@ public class CallManager implements AutoCloseable {
|
||||
var candidates = iceMsg.putArray("candidates");
|
||||
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
|
||||
sendControlMessage(state, writeJson(iceMsg));
|
||||
logger.debug("Forwarded ICE candidate to tunnel for call {}", callId);
|
||||
logger.debug("Forwarded ICE candidate to tunnel for call {}", callIdUnsigned(callId));
|
||||
}
|
||||
|
||||
public void handleIncomingHangup(final long callId) {
|
||||
@ -333,9 +318,25 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
// --- Internal helpers ---
|
||||
|
||||
private CallState getActiveCall(final long callId) throws IOException {
|
||||
var state = activeCalls.get(callId);
|
||||
if (state == null) {
|
||||
throw new IOException("No active call with id " + callIdUnsigned(callId));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private SendMessageResult sendBusyMessage(final long callId, final RecipientId recipientId, final int deviceId) {
|
||||
var busyMessage = new BusyMessage(callId);
|
||||
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, deviceId);
|
||||
return context.getSendHelper().sendCallMessage(callMessage, recipientId);
|
||||
}
|
||||
|
||||
private void sendControlMessage(CallState state, String json) {
|
||||
if (state.controlWriter == null) {
|
||||
logger.debug("Queueing control message for call {} (not yet connected): {}", state.callId, json);
|
||||
logger.debug("Queueing control message for call {} (not yet connected): {}",
|
||||
callIdUnsigned(state.callId),
|
||||
json);
|
||||
state.pendingControlMessages.add(json);
|
||||
return;
|
||||
}
|
||||
@ -345,7 +346,7 @@ public class CallManager implements AutoCloseable {
|
||||
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
|
||||
var proceedMsg = mapper.createObjectNode();
|
||||
proceedMsg.put("type", "proceed");
|
||||
proceedMsg.put("callId", callIdUnsigned(callId));
|
||||
proceedMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||
proceedMsg.put("hideIp", false);
|
||||
var iceServers = proceedMsg.putArray("iceServers");
|
||||
for (var ts : turnServers) {
|
||||
@ -379,8 +380,7 @@ public class CallManager implements AutoCloseable {
|
||||
stdinStream.flush();
|
||||
|
||||
// stdin is the control write channel
|
||||
state.controlWriter = new PrintWriter(
|
||||
new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
|
||||
state.controlWriter = new PrintWriter(new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
|
||||
|
||||
// Flush any pending control messages
|
||||
for (var msg : state.pendingControlMessages) {
|
||||
@ -392,17 +392,17 @@ public class CallManager implements AutoCloseable {
|
||||
sendAcceptIfReady(state);
|
||||
|
||||
// Read control events from subprocess stdout
|
||||
Thread.ofVirtual().name("control-read-" + state.callId).start(() -> {
|
||||
readControlEvents(state, process.getInputStream());
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.name("control-read-" + callIdUnsigned(state.callId))
|
||||
.start(() -> readControlEvents(state, process.getInputStream()));
|
||||
|
||||
// Drain subprocess stderr to prevent pipe buffer deadlock
|
||||
Thread.ofVirtual().name("tunnel-stderr-" + state.callId).start(() -> {
|
||||
try (var reader = new BufferedReader(
|
||||
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
|
||||
Thread.ofVirtual().name("tunnel-stderr-" + callIdUnsigned(state.callId)).start(() -> {
|
||||
try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
logger.debug("[tunnel-{}] {}", state.callId, line);
|
||||
logger.debug("[tunnel-{}] {}", callIdUnsigned(state.callId), line);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
@ -410,15 +410,15 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
// Monitor process exit
|
||||
process.onExit().thenAcceptAsync(p -> {
|
||||
logger.info("Tunnel for call {} exited with code {}", state.callId, p.exitValue());
|
||||
logger.debug("Tunnel for call {} exited with code {}", callIdUnsigned(state.callId), p.exitValue());
|
||||
if (activeCalls.containsKey(state.callId)) {
|
||||
endCall(state.callId, "tunnel_exit");
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("Spawned signal-call-tunnel for call {}", state.callId);
|
||||
logger.debug("Spawned signal-call-tunnel for call {}", callIdUnsigned(state.callId));
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to spawn tunnel for call {}", state.callId, e);
|
||||
logger.error("Failed to spawn tunnel for call {}", callIdUnsigned(state.callId), e);
|
||||
endCall(state.callId, "tunnel_spawn_error");
|
||||
}
|
||||
}
|
||||
@ -461,20 +461,19 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
private String buildConfig(CallState state) {
|
||||
var config = mapper.createObjectNode();
|
||||
config.put("call_id", callIdUnsigned(state.callId));
|
||||
config.put("call_id", Utils.callIdUnsigned(state.callId));
|
||||
config.put("is_outgoing", state.isOutgoing);
|
||||
config.put("local_device_id", 1);
|
||||
return writeJson(config);
|
||||
}
|
||||
|
||||
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
|
||||
try (var reader = new BufferedReader(
|
||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
logger.debug("Control event for call {}: {}", state.callId, line);
|
||||
logger.debug("Control event for call {}: {}", callIdUnsigned(state.callId), line);
|
||||
|
||||
try {
|
||||
var json = mapper.readTree(line);
|
||||
@ -489,17 +488,19 @@ public class CallManager implements AutoCloseable {
|
||||
state.outputDeviceName = json.get("outputDeviceName").asText();
|
||||
}
|
||||
logger.debug("Tunnel ready for call {}: input={}, output={}",
|
||||
state.callId, state.inputDeviceName, state.outputDeviceName);
|
||||
callIdUnsigned(state.callId),
|
||||
state.inputDeviceName,
|
||||
state.outputDeviceName);
|
||||
}
|
||||
case "sendOffer" -> {
|
||||
var opaqueB64 = json.get("opaque").asText();
|
||||
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||
sendOfferViaSignal(state, opaque);
|
||||
logSendMessageResult(sendOfferViaSignal(state, opaque));
|
||||
}
|
||||
case "sendAnswer" -> {
|
||||
var opaqueB64 = json.get("opaque").asText();
|
||||
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||
sendAnswerViaSignal(state, opaque);
|
||||
logSendMessageResult(sendAnswerViaSignal(state, opaque));
|
||||
}
|
||||
case "sendIce" -> {
|
||||
var candidatesArr = json.get("candidates");
|
||||
@ -507,22 +508,24 @@ public class CallManager implements AutoCloseable {
|
||||
for (var c : candidatesArr) {
|
||||
opaqueList.add(java.util.Base64.getDecoder().decode(c.get("opaque").asText()));
|
||||
}
|
||||
sendIceViaSignal(state, opaqueList);
|
||||
logSendMessageResult(sendIceViaSignal(state, opaqueList));
|
||||
}
|
||||
case "sendHangup" -> {
|
||||
// RingRTC wants us to send a hangup message via Signal protocol.
|
||||
// This is NOT a local state change — local state is handled by stateChange events.
|
||||
var hangupType = json.has("hangupType") ? json.get("hangupType").asText("normal") : "normal";
|
||||
var hangupType = json.has("hangupType")
|
||||
? json.get("hangupType").asText("normal")
|
||||
: "normal";
|
||||
// Skip multi-device hangup types — signal-cli is single-device,
|
||||
// and sending these to the remote peer causes it to terminate the call.
|
||||
if (hangupType.contains("onanotherdevice")) {
|
||||
logger.debug("Ignoring multi-device hangup type: {}", hangupType);
|
||||
} else {
|
||||
sendHangupViaSignal(state, hangupType);
|
||||
logSendMessageResult(sendHangupViaSignal(state, hangupType));
|
||||
}
|
||||
}
|
||||
case "sendBusy" -> {
|
||||
sendBusyViaSignal(state);
|
||||
logSendMessageResult(sendBusyViaSignal(state));
|
||||
}
|
||||
case "stateChange" -> {
|
||||
var ringrtcState = json.get("state").asText();
|
||||
@ -531,19 +534,23 @@ public class CallManager implements AutoCloseable {
|
||||
}
|
||||
case "error" -> {
|
||||
var message = json.has("message") ? json.get("message").asText("unknown") : "unknown";
|
||||
logger.error("Tunnel error for call {}: {}", state.callId, message);
|
||||
logger.error("Tunnel error for call {}: {}", callIdUnsigned(state.callId), message);
|
||||
endCall(state.callId, "tunnel_error");
|
||||
}
|
||||
default -> {
|
||||
logger.debug("Unknown control event type '{}' for call {}", type, state.callId);
|
||||
logger.debug("Unknown control event type '{}' for call {}",
|
||||
type,
|
||||
callIdUnsigned(state.callId));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to parse control event JSON for call {}: {}", state.callId, e.getMessage());
|
||||
logger.warn("Failed to parse control event JSON for call {}: {}",
|
||||
callIdUnsigned(state.callId),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Control read ended for call {}: {}", state.callId, e.getMessage());
|
||||
logger.debug("Control read ended for call {}: {}", callIdUnsigned(state.callId), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,114 +580,97 @@ public class CallManager implements AutoCloseable {
|
||||
fireCallEvent(state, reason);
|
||||
}
|
||||
|
||||
public static void logSendMessageResult(SendMessageResult result) {
|
||||
var identifier = result.getAddress().getIdentifier();
|
||||
if (result.getProofRequiredFailure() != null) {
|
||||
final var failure = result.getProofRequiredFailure();
|
||||
logger.warn(
|
||||
"CAPTCHA proof required for sending to \"{}\", available options \"{}\" with challenge token \"{}\", or wait \"{}\" seconds.\n",
|
||||
identifier,
|
||||
failure.getOptions()
|
||||
.stream()
|
||||
.map(ProofRequiredException.Option::toString)
|
||||
.collect(Collectors.joining(", ")),
|
||||
failure.getToken(),
|
||||
failure.getRetryAfterSeconds());
|
||||
} else if (result.isNetworkFailure()) {
|
||||
logger.warn("Network failure for \"{}\"", identifier);
|
||||
} else if (result.getRateLimitFailure() != null) {
|
||||
logger.warn("Rate limit failure for \"{}\"", identifier);
|
||||
} else if (result.isUnregisteredFailure()) {
|
||||
logger.warn("Unregistered user \"{}\"", identifier);
|
||||
} else if (result.getIdentityFailure() != null) {
|
||||
logger.warn("Untrusted Identity for \"{}\"", identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAcceptIfReady(CallState state) {
|
||||
if (state.acceptPending && state.tunnelRinging && state.controlWriter != null) {
|
||||
state.acceptPending = false;
|
||||
logger.debug("Sending deferred accept for call {}", state.callId);
|
||||
logger.debug("Sending deferred accept for call {}", callIdUnsigned(state.callId));
|
||||
var acceptMsg = mapper.createObjectNode();
|
||||
acceptMsg.put("type", "accept");
|
||||
state.controlWriter.println(writeJson(acceptMsg));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendOfferViaSignal(CallState state, byte[] opaque) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var offerMessage = new org.whispersystems.signalservice.api.messages.calls.OfferMessage(state.callId,
|
||||
org.whispersystems.signalservice.api.messages.calls.OfferMessage.Type.AUDIO_CALL,
|
||||
opaque);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forOffer(
|
||||
offerMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
logger.info("Sent offer via Signal for call {}", state.callId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send offer for call {}", state.callId, e);
|
||||
}
|
||||
private SendMessageResult sendOfferViaSignal(CallState state, byte[] opaque) {
|
||||
var offerMessage = new OfferMessage(state.callId, OfferMessage.Type.AUDIO_CALL, opaque);
|
||||
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, state.deviceId);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
logger.debug("Sent offer via Signal for call {}", callIdUnsigned(state.callId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sendAnswerViaSignal(CallState state, byte[] opaque) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var answerMessage = new org.whispersystems.signalservice.api.messages.calls.AnswerMessage(state.callId, opaque);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forAnswer(
|
||||
answerMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
logger.info("Sent answer via Signal for call {}", state.callId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send answer for call {}", state.callId, e);
|
||||
}
|
||||
private SendMessageResult sendAnswerViaSignal(CallState state, byte[] opaque) {
|
||||
var answerMessage = new AnswerMessage(state.callId, opaque);
|
||||
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, state.deviceId);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
logger.debug("Sent answer via Signal for call {}", callIdUnsigned(state.callId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var iceUpdates = opaqueList.stream()
|
||||
.map(opaque -> new org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage(
|
||||
state.callId, opaque))
|
||||
.toList();
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forIceUpdates(
|
||||
iceUpdates, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
logger.info("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), state.callId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send ICE for call {}", state.callId, e);
|
||||
}
|
||||
private SendMessageResult sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
|
||||
var iceUpdates = opaqueList.stream().map(opaque -> new IceUpdateMessage(state.callId, opaque)).toList();
|
||||
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, state.deviceId);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
logger.debug("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), callIdUnsigned(state.callId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sendBusyViaSignal(CallState state) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var busyMessage = new org.whispersystems.signalservice.api.messages.calls.BusyMessage(state.callId);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forBusy(
|
||||
busyMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send busy for call {}", state.callId, e);
|
||||
}
|
||||
private SendMessageResult sendBusyViaSignal(CallState state) {
|
||||
var busyMessage = new BusyMessage(state.callId);
|
||||
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, state.deviceId);
|
||||
return context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
}
|
||||
|
||||
private void sendHangupViaSignal(CallState state, String hangupType) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var type = switch (hangupType) {
|
||||
case "accepted", "acceptedonanotherdevice" ->
|
||||
org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.ACCEPTED;
|
||||
case "declined", "declinedonanotherdevice" ->
|
||||
org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.DECLINED;
|
||||
case "busy", "busyonanotherdevice" ->
|
||||
org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.BUSY;
|
||||
default -> org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.NORMAL;
|
||||
};
|
||||
var hangupMessage = new org.whispersystems.signalservice.api.messages.calls.HangupMessage(
|
||||
state.callId, type, 0);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forHangup(
|
||||
hangupMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
logger.info("Sent hangup ({}) via Signal for call {}", hangupType, state.callId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send hangup for call {}", state.callId, e);
|
||||
}
|
||||
private SendMessageResult sendHangupViaSignal(CallState state, String hangupType) {
|
||||
var type = switch (hangupType) {
|
||||
case "accepted", "acceptedonanotherdevice" -> HangupMessage.Type.ACCEPTED;
|
||||
case "declined", "declinedonanotherdevice" -> HangupMessage.Type.DECLINED;
|
||||
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||
default -> HangupMessage.Type.NORMAL;
|
||||
};
|
||||
var hangupMessage = new HangupMessage(state.callId, type, state.deviceId);
|
||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] getRemoteIdentityKey(CallState state) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(state.recipientId);
|
||||
var serviceId = address.getServiceId();
|
||||
var identityInfo = account.getIdentityKeyStore().getIdentityInfo(serviceId);
|
||||
if (identityInfo != null) {
|
||||
return getRawIdentityKeyBytes(identityInfo.getIdentityKey().serialize());
|
||||
return getRawIdentityKeyBytes(identityInfo.getIdentityKey());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to get remote identity key for call {}", state.callId, e);
|
||||
logger.warn("Failed to get remote identity key for call {}", callIdUnsigned(state.callId), e);
|
||||
}
|
||||
logger.warn("Using local identity key as fallback for remote identity key");
|
||||
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey().serialize());
|
||||
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -688,18 +678,18 @@ public class CallManager implements AutoCloseable {
|
||||
* raw 32-byte Curve25519 public key. Signal Android does this via
|
||||
* WebRtcUtil.getPublicKeyBytes() before passing keys to RingRTC.
|
||||
*/
|
||||
private static byte[] getRawIdentityKeyBytes(byte[] serializedKey) {
|
||||
private static byte[] getRawIdentityKeyBytes(IdentityKey identityKey) {
|
||||
var serializedKey = identityKey.serialize();
|
||||
return getRawIdentityKeyBytes(serializedKey);
|
||||
}
|
||||
|
||||
private static byte[] getRawIdentityKeyBytes(final byte[] serializedKey) {
|
||||
if (serializedKey.length == 33 && serializedKey[0] == 0x05) {
|
||||
return java.util.Arrays.copyOfRange(serializedKey, 1, serializedKey.length);
|
||||
}
|
||||
return serializedKey;
|
||||
}
|
||||
|
||||
/** Convert signed long call ID to unsigned BigInteger (tunnel binary expects u64). */
|
||||
private static BigInteger callIdUnsigned(long callId) {
|
||||
return new BigInteger(Long.toUnsignedString(callId));
|
||||
}
|
||||
|
||||
private static String writeJson(ObjectNode node) {
|
||||
try {
|
||||
return mapper.writeValueAsString(node);
|
||||
@ -714,21 +704,19 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
state.state = CallInfo.State.ENDED;
|
||||
fireCallEvent(state, reason);
|
||||
logger.info("Call {} ended: {}", callId, reason);
|
||||
logger.debug("Call {} ended: {}", callIdUnsigned(callId), reason);
|
||||
|
||||
// Send Signal protocol hangup to remote peer (unless they initiated the end)
|
||||
if (!"remote_hangup".equals(reason) && !"rejected".equals(reason) && !"remote_busy".equals(reason)
|
||||
if (!"remote_hangup".equals(reason)
|
||||
&& !"rejected".equals(reason)
|
||||
&& !"remote_busy".equals(reason)
|
||||
&& !"ringrtc_hangup".equals(reason)) {
|
||||
try {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(state.recipientIdentifier);
|
||||
var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var hangupMessage = new org.whispersystems.signalservice.api.messages.calls.HangupMessage(callId,
|
||||
org.whispersystems.signalservice.api.messages.calls.HangupMessage.Type.NORMAL, 0);
|
||||
var callMessage = org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage.forHangup(
|
||||
hangupMessage, null);
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send hangup to remote for call {}", callId, e);
|
||||
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, state.deviceId);
|
||||
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||
if (!result.isSuccess()) {
|
||||
logger.warn("Failed to send hangup to remote for call {}", callIdUnsigned(callId));
|
||||
logSendMessageResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -755,13 +743,13 @@ public class CallManager implements AutoCloseable {
|
||||
if (state == null) return;
|
||||
|
||||
if (state.state == CallInfo.State.RINGING_INCOMING || state.state == CallInfo.State.RINGING_OUTGOING) {
|
||||
logger.info("Call {} ring timeout", callId);
|
||||
logger.debug("Call {} ring timeout", callIdUnsigned(callId));
|
||||
endCall(callId, "ring_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
private static long generateCallId() {
|
||||
return new SecureRandom().nextLong() & Long.MAX_VALUE;
|
||||
return new BigInteger(64, new SecureRandom()).longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -770,6 +758,9 @@ public class CallManager implements AutoCloseable {
|
||||
for (var callId : new ArrayList<>(activeCalls.keySet())) {
|
||||
endCall(callId, "shutdown");
|
||||
}
|
||||
synchronized (callEventListeners) {
|
||||
callEventListeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal call state tracking ---
|
||||
@ -778,17 +769,15 @@ public class CallManager implements AutoCloseable {
|
||||
|
||||
final long callId;
|
||||
volatile CallInfo.State state;
|
||||
final org.asamk.signal.manager.api.RecipientAddress recipientAddress;
|
||||
final RecipientIdentifier.Single recipientIdentifier;
|
||||
final RecipientId recipientId;
|
||||
volatile Integer deviceId;
|
||||
final boolean isOutgoing;
|
||||
volatile String inputDeviceName;
|
||||
volatile String outputDeviceName;
|
||||
volatile Process tunnelProcess;
|
||||
volatile PrintWriter controlWriter;
|
||||
// Raw offer opaque for incoming calls (forwarded to subprocess)
|
||||
volatile byte[] rawOfferOpaque;
|
||||
// Control messages queued before the tunnel process starts
|
||||
final List<String> pendingControlMessages = java.util.Collections.synchronizedList(new ArrayList<>());
|
||||
final List<String> pendingControlMessages = Collections.synchronizedList(new ArrayList<>());
|
||||
// Accept deferred until tunnel reports Ringing state
|
||||
volatile boolean acceptPending = false;
|
||||
// True once the tunnel has reported "Ringing" (ready to accept)
|
||||
@ -797,19 +786,24 @@ public class CallManager implements AutoCloseable {
|
||||
CallState(
|
||||
long callId,
|
||||
CallInfo.State state,
|
||||
org.asamk.signal.manager.api.RecipientAddress recipientAddress,
|
||||
RecipientIdentifier.Single recipientIdentifier,
|
||||
RecipientId recipientId,
|
||||
final Integer deviceId,
|
||||
boolean isOutgoing
|
||||
) {
|
||||
this.callId = callId;
|
||||
this.state = state;
|
||||
this.recipientAddress = recipientAddress;
|
||||
this.recipientIdentifier = recipientIdentifier;
|
||||
this.recipientId = recipientId;
|
||||
this.deviceId = deviceId;
|
||||
this.isOutgoing = isOutgoing;
|
||||
}
|
||||
|
||||
CallInfo toCallInfo() {
|
||||
return new CallInfo(callId, state, recipientAddress, inputDeviceName, outputDeviceName, isOutgoing);
|
||||
CallInfo toCallInfo(RecipientAddressResolver addressResolver) {
|
||||
return new CallInfo(callId,
|
||||
state,
|
||||
addressResolver.resolveRecipientAddress(recipientId).toApiRecipientAddress(),
|
||||
inputDeviceName,
|
||||
outputDeviceName,
|
||||
isOutgoing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,6 +64,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
@ -402,27 +403,33 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
|
||||
if (content.getCallMessage().isPresent()) {
|
||||
handleCallMessage(content.getCallMessage().get(), sender);
|
||||
handleCallMessage(content.getCallMessage().get(), sender, senderDeviceId);
|
||||
}
|
||||
|
||||
return new Pair<>(actions, longTexts);
|
||||
}
|
||||
|
||||
private void handleCallMessage(
|
||||
final org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage callMessage,
|
||||
final org.asamk.signal.manager.storage.recipients.RecipientId sender
|
||||
final SignalServiceCallMessage callMessage,
|
||||
final RecipientId sender,
|
||||
final int deviceId
|
||||
) {
|
||||
var callManager = context.getCallManager();
|
||||
if (callMessage.getDestinationDeviceId().isPresent()
|
||||
&& callMessage.getDestinationDeviceId().get() != account.getDeviceId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
callMessage.getOfferMessage().ifPresent(offer -> {
|
||||
var type = offer.getType() == org.whispersystems.signalservice.api.messages.calls.OfferMessage.Type.VIDEO_CALL
|
||||
var type = offer.getType()
|
||||
== org.whispersystems.signalservice.api.messages.calls.OfferMessage.Type.VIDEO_CALL
|
||||
? org.asamk.signal.manager.api.MessageEnvelope.Call.Offer.Type.VIDEO_CALL
|
||||
: org.asamk.signal.manager.api.MessageEnvelope.Call.Offer.Type.AUDIO_CALL;
|
||||
callManager.handleIncomingOffer(sender, offer.getId(), type, offer.getOpaque());
|
||||
callManager.handleIncomingOffer(sender, deviceId, offer.getId(), type, offer.getOpaque());
|
||||
});
|
||||
|
||||
callMessage.getAnswerMessage().ifPresent(answer ->
|
||||
callManager.handleIncomingAnswer(answer.getId(), answer.getOpaque()));
|
||||
callMessage.getAnswerMessage()
|
||||
.ifPresent(answer -> callManager.handleIncomingAnswer(answer.getId(), deviceId, answer.getOpaque()));
|
||||
|
||||
callMessage.getIceUpdateMessages().ifPresent(iceUpdates -> {
|
||||
for (var ice : iceUpdates) {
|
||||
@ -440,8 +447,7 @@ public final class IncomingMessageHandler {
|
||||
}
|
||||
});
|
||||
|
||||
callMessage.getBusyMessage().ifPresent(busy ->
|
||||
callManager.handleIncomingBusy(busy.getId()));
|
||||
callMessage.getBusyMessage().ifPresent(busy -> callManager.handleIncomingBusy(busy.getId()));
|
||||
}
|
||||
|
||||
private boolean handlePniSignatureMessage(
|
||||
|
||||
@ -36,6 +36,7 @@ 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.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
@ -309,6 +310,26 @@ public class SendHelper {
|
||||
return result;
|
||||
}
|
||||
|
||||
public SendMessageResult sendCallMessage(
|
||||
final SignalServiceCallMessage callMessage,
|
||||
final RecipientId recipientId
|
||||
) {
|
||||
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||
final var result = handleSendMessage(recipientId,
|
||||
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendCallMessage(
|
||||
address,
|
||||
unidentifiedAccess,
|
||||
callMessage));
|
||||
if (callMessage.getTimestamp().isPresent()) {
|
||||
messageSendLogStore.insertIfPossible(callMessage.getTimestamp().get(),
|
||||
result,
|
||||
ContentHint.IMPLICIT,
|
||||
callMessage.isUrgent());
|
||||
}
|
||||
handleSendMessageResult(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SendMessageResult> sendAsGroupMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final GroupInfo g,
|
||||
|
||||
@ -21,7 +21,6 @@ import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.CallOffer;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.Configuration;
|
||||
@ -65,6 +64,7 @@ import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||
import org.asamk.signal.manager.api.TextStyle;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.TypingAction;
|
||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.api.UpdateGroup;
|
||||
@ -172,7 +172,6 @@ public class ManagerImpl implements Manager {
|
||||
private boolean isReceivingSynchronous;
|
||||
private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
|
||||
private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
|
||||
private final Set<CallEventListener> callEventListeners = new HashSet<>();
|
||||
private final List<Runnable> closedListeners = new ArrayList<>();
|
||||
private final List<Runnable> addressChangedListeners = new ArrayList<>();
|
||||
private final CompositeDisposable disposable = new CompositeDisposable();
|
||||
@ -961,11 +960,9 @@ public class ManagerImpl implements Manager {
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
||||
for (final var recipient : recipients) {
|
||||
if (recipient instanceof RecipientIdentifier.Uuid(var uuid)) {
|
||||
account.getMessageSendLogStore()
|
||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(uuid));
|
||||
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(uuid));
|
||||
} else if (recipient instanceof RecipientIdentifier.Pni(var pni)) {
|
||||
account.getMessageSendLogStore()
|
||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni));
|
||||
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni));
|
||||
} else if (recipient instanceof RecipientIdentifier.Single r) {
|
||||
try {
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
|
||||
@ -1717,17 +1714,11 @@ public class ManagerImpl implements Manager {
|
||||
|
||||
@Override
|
||||
public void addCallEventListener(final CallEventListener listener) {
|
||||
synchronized (callEventListeners) {
|
||||
callEventListeners.add(listener);
|
||||
}
|
||||
context.getCallManager().addCallEventListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCallEventListener(final CallEventListener listener) {
|
||||
synchronized (callEventListeners) {
|
||||
callEventListeners.remove(listener);
|
||||
}
|
||||
context.getCallManager().removeCallEventListener(listener);
|
||||
}
|
||||
|
||||
@ -1792,7 +1783,8 @@ public class ManagerImpl implements Manager {
|
||||
|
||||
@Override
|
||||
public CallInfo startCall(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
|
||||
return context.getCallManager().startOutgoingCall(recipient);
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
return context.getCallManager().startOutgoingCall(recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1806,8 +1798,9 @@ public class ManagerImpl implements Manager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectCall(final long callId) throws IOException {
|
||||
context.getCallManager().rejectCall(callId);
|
||||
public SendMessageResult rejectCall(final long callId) throws IOException {
|
||||
final var result = context.getCallManager().rejectCall(callId);
|
||||
return toSendMessageResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1858,9 +1851,7 @@ public class ManagerImpl implements Manager {
|
||||
) throws IOException, UnregisteredRecipientException {
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
var iceUpdates = iceCandidates.stream()
|
||||
.map(opaque -> new IceUpdateMessage(callId, opaque))
|
||||
.toList();
|
||||
var iceUpdates = iceCandidates.stream().map(opaque -> new IceUpdateMessage(callId, opaque)).toList();
|
||||
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, null);
|
||||
try {
|
||||
dependencies.getMessageSender().sendCallMessage(address, null, callMessage);
|
||||
@ -1926,12 +1917,6 @@ public class ManagerImpl implements Manager {
|
||||
if (thread != null) {
|
||||
stopReceiveThread(thread);
|
||||
}
|
||||
synchronized (callEventListeners) {
|
||||
for (var listener : callEventListeners) {
|
||||
context.getCallManager().removeCallEventListener(listener);
|
||||
}
|
||||
callEventListeners.clear();
|
||||
}
|
||||
context.close();
|
||||
executor.close();
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
@ -235,4 +236,11 @@ public class Utils {
|
||||
return proxies.getFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert signed long call ID to unsigned BigInteger (tunnel binary expects u64).
|
||||
*/
|
||||
public static BigInteger callIdUnsigned(long callId) {
|
||||
return new BigInteger(Long.toUnsignedString(callId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.RecipientAddress;
|
||||
|
||||
import org.asamk.signal.manager.storage.recipients.TestRecipientId;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
@ -29,17 +29,23 @@ class CallManagerTest {
|
||||
private static final MethodHandle CALL_ID_UNSIGNED;
|
||||
private static final MethodHandle GENERATE_CALL_ID;
|
||||
|
||||
final RecipientAddressResolver recipientAddressResolver = (id) -> new org.asamk.signal.manager.storage.recipients.RecipientAddress(
|
||||
id.toString());
|
||||
|
||||
static {
|
||||
try {
|
||||
var lookup = MethodHandles.privateLookupIn(CallManager.class, MethodHandles.lookup());
|
||||
|
||||
GET_RAW_IDENTITY_KEY_BYTES = lookup.findStatic(CallManager.class, "getRawIdentityKeyBytes",
|
||||
GET_RAW_IDENTITY_KEY_BYTES = lookup.findStatic(CallManager.class,
|
||||
"getRawIdentityKeyBytes",
|
||||
MethodType.methodType(byte[].class, byte[].class));
|
||||
|
||||
CALL_ID_UNSIGNED = lookup.findStatic(CallManager.class, "callIdUnsigned",
|
||||
CALL_ID_UNSIGNED = lookup.findStatic(Utils.class,
|
||||
"callIdUnsigned",
|
||||
MethodType.methodType(BigInteger.class, long.class));
|
||||
|
||||
GENERATE_CALL_ID = lookup.findStatic(CallManager.class, "generateCallId",
|
||||
GENERATE_CALL_ID = lookup.findStatic(CallManager.class,
|
||||
"generateCallId",
|
||||
MethodType.methodType(long.class));
|
||||
|
||||
} catch (ReflectiveOperationException e) {
|
||||
@ -62,14 +68,7 @@ class CallManagerTest {
|
||||
// --- Helper to create a minimal CallState for state machine tests ---
|
||||
|
||||
private static CallManager.CallState makeCallState(long callId, CallInfo.State initialState) {
|
||||
var address = new RecipientAddress("a1b2c3d4-e5f6-7890-abcd-ef1234567890", null, "+15551234567", null);
|
||||
return new CallManager.CallState(
|
||||
callId,
|
||||
initialState,
|
||||
address,
|
||||
new org.asamk.signal.manager.api.RecipientIdentifier.Number("+15551234567"),
|
||||
true
|
||||
);
|
||||
return new CallManager.CallState(callId, initialState, TestRecipientId.createTestId(15551234567L), null, true);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@ -165,14 +164,6 @@ class CallManagerTest {
|
||||
// generateCallId tests
|
||||
// ========================================================================
|
||||
|
||||
@Test
|
||||
void generateCallId_alwaysNonNegative() throws Throwable {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
long id = generateCallId();
|
||||
assertTrue(id >= 0, "generateCallId returned negative: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateCallId_producesVariation() throws Throwable {
|
||||
long first = generateCallId();
|
||||
@ -336,11 +327,11 @@ class CallManagerTest {
|
||||
state.inputDeviceName = "test_input";
|
||||
state.outputDeviceName = "test_output";
|
||||
|
||||
var info = state.toCallInfo();
|
||||
var info = state.toCallInfo(recipientAddressResolver);
|
||||
|
||||
assertEquals(42L, info.callId());
|
||||
assertEquals(CallInfo.State.CONNECTED, info.state());
|
||||
assertEquals("+15551234567", info.recipient().number().orElse(null));
|
||||
assertEquals("RecipientId[id=15551234567]", info.recipient().number().orElse(null));
|
||||
assertTrue(info.isOutgoing());
|
||||
assertEquals("test_input", info.inputDeviceName());
|
||||
assertEquals("test_output", info.outputDeviceName());
|
||||
@ -350,7 +341,7 @@ class CallManagerTest {
|
||||
void callState_toCallInfoNullDeviceNames() {
|
||||
var state = makeCallState(1L, CallInfo.State.RINGING_INCOMING);
|
||||
|
||||
var info = state.toCallInfo();
|
||||
var info = state.toCallInfo(recipientAddressResolver);
|
||||
|
||||
assertEquals(CallInfo.State.RINGING_INCOMING, info.state());
|
||||
assertEquals(null, info.inputDeviceName());
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
package org.asamk.signal.manager.storage.recipients;
|
||||
|
||||
public class TestRecipientId {
|
||||
|
||||
public static RecipientId createTestId(long value) {
|
||||
return new RecipientId(value, null);
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ import org.slf4j.helpers.MessageFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
|
||||
|
||||
public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
||||
|
||||
final Manager m;
|
||||
@ -297,26 +299,32 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
|
||||
}
|
||||
if (callMessage.answer().isPresent()) {
|
||||
var answerMessage = callMessage.answer().get();
|
||||
writer.println("Answer message: {}, opaque length: {})", answerMessage.id(), answerMessage.opaque().length);
|
||||
writer.println("Answer message: {}, opaque length: {})",
|
||||
callIdUnsigned(answerMessage.id()),
|
||||
answerMessage.opaque().length);
|
||||
}
|
||||
if (callMessage.busy().isPresent()) {
|
||||
var busyMessage = callMessage.busy().get();
|
||||
writer.println("Busy message: {}", busyMessage.id());
|
||||
writer.println("Busy message: {}", callIdUnsigned(busyMessage.id()));
|
||||
}
|
||||
if (callMessage.hangup().isPresent()) {
|
||||
var hangupMessage = callMessage.hangup().get();
|
||||
writer.println("Hangup message: {}", hangupMessage.id());
|
||||
writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id()));
|
||||
}
|
||||
if (!callMessage.iceUpdate().isEmpty()) {
|
||||
writer.println("Ice update messages:");
|
||||
var iceUpdateMessages = callMessage.iceUpdate();
|
||||
for (var iceUpdateMessage : iceUpdateMessages) {
|
||||
writer.println("- {}, opaque length: {}", iceUpdateMessage.id(), iceUpdateMessage.opaque().length);
|
||||
writer.println("- {}, opaque length: {}",
|
||||
callIdUnsigned(iceUpdateMessage.id()),
|
||||
iceUpdateMessage.opaque().length);
|
||||
}
|
||||
}
|
||||
if (callMessage.offer().isPresent()) {
|
||||
var offerMessage = callMessage.offer().get();
|
||||
writer.println("Offer message: {}, opaque length: {}", offerMessage.id(), offerMessage.opaque().length);
|
||||
writer.println("Offer message: {}, opaque length: {}",
|
||||
callIdUnsigned(offerMessage.id()),
|
||||
offerMessage.opaque().length);
|
||||
}
|
||||
if (callMessage.opaque().isPresent()) {
|
||||
final var opaqueMessage = callMessage.opaque().get();
|
||||
|
||||
@ -4,6 +4,8 @@ import org.asamk.Signal;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.CallOffer;
|
||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.Configuration;
|
||||
import org.asamk.signal.manager.api.Contact;
|
||||
@ -37,12 +39,14 @@ import org.asamk.signal.manager.api.Recipient;
|
||||
import org.asamk.signal.manager.api.RecipientAddress;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||
import org.asamk.signal.manager.api.SendMessageResult;
|
||||
import org.asamk.signal.manager.api.SendMessageResults;
|
||||
import org.asamk.signal.manager.api.StickerPack;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||
import org.asamk.signal.manager.api.TrustLevel;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.TypingAction;
|
||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.api.UpdateGroup;
|
||||
@ -925,12 +929,12 @@ public class DbusManagerImpl implements Manager {
|
||||
// --- Voice call methods (not supported over DBus) ---
|
||||
|
||||
@Override
|
||||
public org.asamk.signal.manager.api.CallInfo startCall(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient) {
|
||||
public CallInfo startCall(final RecipientIdentifier.Single recipient) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.asamk.signal.manager.api.CallInfo acceptCall(final long callId) {
|
||||
public CallInfo acceptCall(final long callId) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@ -940,42 +944,54 @@ public class DbusManagerImpl implements Manager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectCall(final long callId) {
|
||||
public SendMessageResult rejectCall(final long callId) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.asamk.signal.manager.api.CallInfo> listActiveCalls() {
|
||||
public java.util.List<CallInfo> listActiveCalls() {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCallOffer(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient, final org.asamk.signal.manager.api.CallOffer offer) {
|
||||
public void sendCallOffer(final RecipientIdentifier.Single recipient, final CallOffer offer) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCallAnswer(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient, final long callId, final byte[] answerOpaque) {
|
||||
public void sendCallAnswer(
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final long callId,
|
||||
final byte[] answerOpaque
|
||||
) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendIceUpdate(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient, final long callId, final java.util.List<byte[]> iceCandidates) {
|
||||
public void sendIceUpdate(
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final long callId,
|
||||
final java.util.List<byte[]> iceCandidates
|
||||
) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendHangup(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient, final long callId, final org.asamk.signal.manager.api.MessageEnvelope.Call.Hangup.Type type) {
|
||||
public void sendHangup(
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final long callId,
|
||||
final MessageEnvelope.Call.Hangup.Type type
|
||||
) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBusy(final org.asamk.signal.manager.api.RecipientIdentifier.Single recipient, final long callId) {
|
||||
public void sendBusy(final RecipientIdentifier.Single recipient, final long callId) {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.asamk.signal.manager.api.TurnServer> getTurnServerInfo() {
|
||||
public java.util.List<TurnServer> getTurnServerInfo() {
|
||||
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,12 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
|
||||
|
||||
record JsonCallMessage(
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL) Offer offerMessage,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL) Answer answerMessage,
|
||||
@ -23,38 +26,41 @@ record JsonCallMessage(
|
||||
callMessage.iceUpdate().stream().map(IceUpdate::from).toList());
|
||||
}
|
||||
|
||||
record Offer(long id, String type, String opaque) {
|
||||
record Offer(BigInteger id, String type, String opaque) {
|
||||
|
||||
public static Offer from(final MessageEnvelope.Call.Offer offer) {
|
||||
return new Offer(offer.id(), offer.type().name(), Base64.getEncoder().encodeToString(offer.opaque()));
|
||||
return new Offer(callIdUnsigned(offer.id()),
|
||||
offer.type().name(),
|
||||
Base64.getEncoder().encodeToString(offer.opaque()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Answer(long id, String opaque) {
|
||||
public record Answer(BigInteger id, String opaque) {
|
||||
|
||||
public static Answer from(final MessageEnvelope.Call.Answer answer) {
|
||||
return new Answer(answer.id(), Base64.getEncoder().encodeToString(answer.opaque()));
|
||||
return new Answer(callIdUnsigned(answer.id()), Base64.getEncoder().encodeToString(answer.opaque()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Busy(long id) {
|
||||
public record Busy(BigInteger id) {
|
||||
|
||||
public static Busy from(final MessageEnvelope.Call.Busy busy) {
|
||||
return new Busy(busy.id());
|
||||
return new Busy(callIdUnsigned(busy.id()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Hangup(long id, String type, int deviceId) {
|
||||
public record Hangup(BigInteger id, String type, int deviceId) {
|
||||
|
||||
public static Hangup from(final MessageEnvelope.Call.Hangup hangup) {
|
||||
return new Hangup(hangup.id(), hangup.type().name(), hangup.deviceId());
|
||||
return new Hangup(callIdUnsigned(hangup.id()), hangup.type().name(), hangup.deviceId());
|
||||
}
|
||||
}
|
||||
|
||||
public record IceUpdate(long id, String opaque) {
|
||||
public record IceUpdate(BigInteger id, String opaque) {
|
||||
|
||||
public static IceUpdate from(final MessageEnvelope.Call.IceUpdate iceUpdate) {
|
||||
return new IceUpdate(iceUpdate.id(), Base64.getEncoder().encodeToString(iceUpdate.opaque()));
|
||||
return new IceUpdate(callIdUnsigned(iceUpdate.id()),
|
||||
Base64.getEncoder().encodeToString(iceUpdate.opaque()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1082,6 +1082,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "java.math.BigInteger"
|
||||
},
|
||||
{
|
||||
"type": "java.math.BigInteger[]"
|
||||
},
|
||||
{
|
||||
"type": "java.net.NetPermission"
|
||||
},
|
||||
@ -1977,10 +1983,18 @@
|
||||
"name": "codec",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "inputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "mediaSocketPath",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "outputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "ptimeMs",
|
||||
"parameterTypes": []
|
||||
@ -2064,6 +2078,10 @@
|
||||
"name": "callId",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "inputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "isOutgoing",
|
||||
"parameterTypes": []
|
||||
@ -2076,6 +2094,10 @@
|
||||
"name": "number",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "outputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"parameterTypes": []
|
||||
@ -2297,10 +2319,18 @@
|
||||
"name": "codec",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "inputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "mediaSocketPath",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "outputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "ptimeMs",
|
||||
"parameterTypes": []
|
||||
@ -2421,6 +2451,43 @@
|
||||
{
|
||||
"type": "org.asamk.signal.json.JsonAttachment[]"
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.json.JsonCallEvent",
|
||||
"methods": [
|
||||
{
|
||||
"name": "callId",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "inputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "isOutgoing",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "number",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "outputDeviceName",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "reason",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"parameterTypes": []
|
||||
},
|
||||
{
|
||||
"name": "uuid",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.asamk.signal.json.JsonCallMessage",
|
||||
"allDeclaredFields": true,
|
||||
@ -7484,6 +7551,35 @@
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.api.messages.calls.TurnServerInfo",
|
||||
"fields": [
|
||||
{
|
||||
"name": "hostname"
|
||||
},
|
||||
{
|
||||
"name": "password"
|
||||
},
|
||||
{
|
||||
"name": "ttl"
|
||||
},
|
||||
{
|
||||
"name": "urls"
|
||||
},
|
||||
{
|
||||
"name": "urlsWithIps"
|
||||
},
|
||||
{
|
||||
"name": "username"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo",
|
||||
"allDeclaredFields": true,
|
||||
@ -8064,6 +8160,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.push.GetCallingRelaysResponse",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"java.util.List"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "org.whispersystems.signalservice.internal.push.GetUsernameFromLinkResponseBody",
|
||||
"allDeclaredFields": true,
|
||||
|
||||
@ -2,25 +2,57 @@ package org.asamk.signal.jsonrpc;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.MultiAccountManager;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.ProvisioningManager;
|
||||
import org.asamk.signal.manager.api.*;
|
||||
import org.asamk.signal.manager.RegistrationManager;
|
||||
import org.asamk.signal.manager.api.CallInfo;
|
||||
import org.asamk.signal.manager.api.CallOffer;
|
||||
import org.asamk.signal.manager.api.Configuration;
|
||||
import org.asamk.signal.manager.api.Device;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.Group;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.api.Identity;
|
||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||
import org.asamk.signal.manager.api.Message;
|
||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||
import org.asamk.signal.manager.api.Recipient;
|
||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||
import org.asamk.signal.manager.api.SendMessageResult;
|
||||
import org.asamk.signal.manager.api.SendMessageResults;
|
||||
import org.asamk.signal.manager.api.StickerPack;
|
||||
import org.asamk.signal.manager.api.StickerPackId;
|
||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||
import org.asamk.signal.manager.api.TurnServer;
|
||||
import org.asamk.signal.manager.api.TypingAction;
|
||||
import org.asamk.signal.manager.api.UpdateGroup;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
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.output.JsonWriter;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests for the subscribeCallEvents / unsubscribeCallEvents JSON-RPC commands
|
||||
@ -32,6 +64,7 @@ class SubscribeCallEventsTest {
|
||||
* Feeds pre-configured JSON-RPC lines to the handler, then returns null to end.
|
||||
*/
|
||||
private static class LineFeeder {
|
||||
|
||||
private final Queue<String> lines = new ConcurrentLinkedQueue<>();
|
||||
|
||||
void addLine(String line) {
|
||||
@ -47,6 +80,7 @@ class SubscribeCallEventsTest {
|
||||
* Captures JSON-RPC responses written by the handler.
|
||||
*/
|
||||
private static class CapturingJsonWriter implements JsonWriter {
|
||||
|
||||
final List<Object> written = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
@Override
|
||||
@ -59,6 +93,7 @@ class SubscribeCallEventsTest {
|
||||
* Minimal Manager stub that tracks call event listener add/remove calls.
|
||||
*/
|
||||
private static class StubManager implements Manager {
|
||||
|
||||
final List<CallEventListener> listeners = new ArrayList<>();
|
||||
final AtomicInteger addCount = new AtomicInteger(0);
|
||||
final AtomicInteger removeCount = new AtomicInteger(0);
|
||||
@ -68,113 +103,483 @@ class SubscribeCallEventsTest {
|
||||
this.selfNumber = selfNumber;
|
||||
}
|
||||
|
||||
@Override public void addCallEventListener(CallEventListener listener) {
|
||||
@Override
|
||||
public void addCallEventListener(CallEventListener listener) {
|
||||
addCount.incrementAndGet();
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override public void removeCallEventListener(CallEventListener listener) {
|
||||
@Override
|
||||
public void removeCallEventListener(CallEventListener listener) {
|
||||
removeCount.incrementAndGet();
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override public String getSelfNumber() { return selfNumber; }
|
||||
@Override
|
||||
public String getSelfNumber() {
|
||||
return selfNumber;
|
||||
}
|
||||
|
||||
// --- Stubs for remaining Manager interface methods ---
|
||||
@Override public Map<String, UserStatus> getUserStatus(Set<String> n) { return Map.of(); }
|
||||
@Override public Map<String, UsernameStatus> getUsernameStatus(Set<String> u) { return Map.of(); }
|
||||
@Override public void updateAccountAttributes(String d, Boolean u, Boolean dn, Boolean ns) {}
|
||||
@Override public Configuration getConfiguration() { return null; }
|
||||
@Override public void updateConfiguration(Configuration c) {}
|
||||
@Override public void updateProfile(UpdateProfile u) {}
|
||||
@Override public String getUsername() { return null; }
|
||||
@Override public UsernameLinkUrl getUsernameLink() { return null; }
|
||||
@Override public void setUsername(String u) {}
|
||||
@Override public void deleteUsername() {}
|
||||
@Override public void startChangeNumber(String n, boolean v, String c) {}
|
||||
@Override public void finishChangeNumber(String n, String v, String p) {}
|
||||
@Override public void unregister() {}
|
||||
@Override public void deleteAccount() {}
|
||||
@Override public void submitRateLimitRecaptchaChallenge(String c, String cap) {}
|
||||
@Override public List<Device> getLinkedDevices() { return List.of(); }
|
||||
@Override public void updateLinkedDevice(int d, String n) {}
|
||||
@Override public void removeLinkedDevices(int d) {}
|
||||
@Override public void addDeviceLink(DeviceLinkUrl u) {}
|
||||
@Override public void setRegistrationLockPin(Optional<String> p) {}
|
||||
@Override public List<Group> getGroups() { return List.of(); }
|
||||
@Override public List<Group> getGroups(Collection<GroupId> g) { return List.of(); }
|
||||
@Override public SendGroupMessageResults quitGroup(GroupId g, Set<RecipientIdentifier.Single> a) { return null; }
|
||||
@Override public void deleteGroup(GroupId g) {}
|
||||
@Override public Pair<GroupId, SendGroupMessageResults> createGroup(String n, Set<RecipientIdentifier.Single> m, String a) { return null; }
|
||||
@Override public SendGroupMessageResults updateGroup(GroupId g, UpdateGroup u) { return null; }
|
||||
@Override public Pair<GroupId, SendGroupMessageResults> joinGroup(GroupInviteLinkUrl u) { return null; }
|
||||
@Override public SendMessageResults sendTypingMessage(TypingAction a, Set<RecipientIdentifier> r) { return null; }
|
||||
@Override public SendMessageResults sendReadReceipt(RecipientIdentifier.Single s, List<Long> m) { return null; }
|
||||
@Override public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single s, List<Long> m) { return null; }
|
||||
@Override public SendMessageResults sendMessage(Message m, Set<RecipientIdentifier> r, boolean n) { return null; }
|
||||
@Override public SendMessageResults sendEditMessage(Message m, Set<RecipientIdentifier> r, long t) { return null; }
|
||||
@Override public SendMessageResults sendRemoteDeleteMessage(long t, Set<RecipientIdentifier> r) { return null; }
|
||||
@Override public SendMessageResults sendMessageReaction(String e, boolean rm, RecipientIdentifier.Single a, long t, Set<RecipientIdentifier> r, boolean n, boolean s) { return null; }
|
||||
@Override public SendMessageResults sendAdminDelete(RecipientIdentifier.Single a, long t, Set<RecipientIdentifier.Group> r, boolean n, boolean s) { return null; }
|
||||
@Override public SendMessageResults sendPinMessage(int d, RecipientIdentifier.Single a, long t, Set<RecipientIdentifier> r, boolean n, boolean s) { return null; }
|
||||
@Override public SendMessageResults sendUnpinMessage(RecipientIdentifier.Single a, long t, Set<RecipientIdentifier> r, boolean n, boolean s) { return null; }
|
||||
@Override public SendMessageResults sendPaymentNotificationMessage(byte[] r, String n, RecipientIdentifier.Single re) { return null; }
|
||||
@Override public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> r) { return null; }
|
||||
@Override public SendMessageResults sendMessageRequestResponse(MessageEnvelope.Sync.MessageRequestResponse.Type t, Set<RecipientIdentifier> r) { return null; }
|
||||
@Override public SendMessageResults sendPollCreateMessage(String q, boolean a, List<String> o, Set<RecipientIdentifier> r, boolean n) { return null; }
|
||||
@Override public SendMessageResults sendPollVoteMessage(RecipientIdentifier.Single a, long t, List<Integer> o, int v, Set<RecipientIdentifier> r, boolean n) { return null; }
|
||||
@Override public SendMessageResults sendPollTerminateMessage(long t, Set<RecipientIdentifier> r, boolean n) { return null; }
|
||||
@Override public void hideRecipient(RecipientIdentifier.Single r) {}
|
||||
@Override public void deleteRecipient(RecipientIdentifier.Single r) {}
|
||||
@Override public void deleteContact(RecipientIdentifier.Single r) {}
|
||||
@Override public void setContactName(RecipientIdentifier.Single r, String g, String f, String ng, String nf, String n) {}
|
||||
@Override public void setContactsBlocked(Collection<RecipientIdentifier.Single> r, boolean b) {}
|
||||
@Override public void setGroupsBlocked(Collection<GroupId> g, boolean b) {}
|
||||
@Override public void setExpirationTimer(RecipientIdentifier.Single r, int t) {}
|
||||
@Override public StickerPackUrl uploadStickerPack(File p) { return null; }
|
||||
@Override public void installStickerPack(StickerPackUrl u) {}
|
||||
@Override public List<StickerPack> getStickerPacks() { return List.of(); }
|
||||
@Override public void requestAllSyncData() {}
|
||||
@Override public void addReceiveHandler(ReceiveMessageHandler h, boolean w) {}
|
||||
@Override public void removeReceiveHandler(ReceiveMessageHandler h) {}
|
||||
@Override public boolean isReceiving() { return false; }
|
||||
@Override public void receiveMessages(Optional<Duration> t, Optional<Integer> m, ReceiveMessageHandler h) {}
|
||||
@Override public void stopReceiveMessages() {}
|
||||
@Override public void setReceiveConfig(ReceiveConfig r) {}
|
||||
@Override public boolean isContactBlocked(RecipientIdentifier.Single r) { return false; }
|
||||
@Override public void sendContacts() {}
|
||||
@Override public List<Recipient> getRecipients(boolean o, Optional<Boolean> b, Collection<RecipientIdentifier.Single> a, Optional<String> n) { return List.of(); }
|
||||
@Override public String getContactOrProfileName(RecipientIdentifier.Single r) { return null; }
|
||||
@Override public Group getGroup(GroupId g) { return null; }
|
||||
@Override public List<Identity> getIdentities() { return List.of(); }
|
||||
@Override public List<Identity> getIdentities(RecipientIdentifier.Single r) { return List.of(); }
|
||||
@Override public boolean trustIdentityVerified(RecipientIdentifier.Single r, IdentityVerificationCode v) { return false; }
|
||||
@Override public boolean trustIdentityAllKeys(RecipientIdentifier.Single r) { return false; }
|
||||
@Override public void addAddressChangedListener(Runnable l) {}
|
||||
@Override public void addClosedListener(Runnable l) {}
|
||||
@Override public InputStream retrieveAttachment(String id) { return null; }
|
||||
@Override public InputStream retrieveContactAvatar(RecipientIdentifier.Single r) { return null; }
|
||||
@Override public InputStream retrieveProfileAvatar(RecipientIdentifier.Single r) { return null; }
|
||||
@Override public InputStream retrieveGroupAvatar(GroupId g) { return null; }
|
||||
@Override public InputStream retrieveSticker(StickerPackId s, int i) { return null; }
|
||||
@Override public CallInfo startCall(RecipientIdentifier.Single r) { return null; }
|
||||
@Override public CallInfo acceptCall(long c) { return null; }
|
||||
@Override public void hangupCall(long c) {}
|
||||
@Override public void rejectCall(long c) {}
|
||||
@Override public List<CallInfo> listActiveCalls() { return List.of(); }
|
||||
@Override public void sendCallOffer(RecipientIdentifier.Single r, CallOffer o) {}
|
||||
@Override public void sendCallAnswer(RecipientIdentifier.Single r, long c, byte[] a) {}
|
||||
@Override public void sendIceUpdate(RecipientIdentifier.Single r, long c, List<byte[]> i) {}
|
||||
@Override public void sendHangup(RecipientIdentifier.Single r, long c, MessageEnvelope.Call.Hangup.Type t) {}
|
||||
@Override public void sendBusy(RecipientIdentifier.Single r, long c) {}
|
||||
@Override public List<TurnServer> getTurnServerInfo() { return List.of(); }
|
||||
@Override public void close() {}
|
||||
@Override
|
||||
public Map<String, UserStatus> getUserStatus(Set<String> n) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, UsernameStatus> getUsernameStatus(Set<String> u) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAccountAttributes(String d, Boolean u, Boolean dn, Boolean ns) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration getConfiguration() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateConfiguration(Configuration c) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProfile(UpdateProfile u) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernameLinkUrl getUsernameLink() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String u) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUsername() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startChangeNumber(String n, boolean v, String c) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishChangeNumber(String n, String v, String p) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccount() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitRateLimitRecaptchaChallenge(String c, String cap) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Device> getLinkedDevices() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLinkedDevice(int d, String n) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLinkedDevices(int d) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDeviceLink(DeviceLinkUrl u) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRegistrationLockPin(Optional<String> p) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Group> getGroups() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Group> getGroups(Collection<GroupId> g) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendGroupMessageResults quitGroup(GroupId g, Set<RecipientIdentifier.Single> a) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteGroup(GroupId g) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String n,
|
||||
Set<RecipientIdentifier.Single> m,
|
||||
String a
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendGroupMessageResults updateGroup(GroupId g, UpdateGroup u) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<GroupId, SendGroupMessageResults> joinGroup(GroupInviteLinkUrl u) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendTypingMessage(TypingAction a, Set<RecipientIdentifier> r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single s, List<Long> m) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single s, List<Long> m) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendMessage(Message m, Set<RecipientIdentifier> r, boolean n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendEditMessage(Message m, Set<RecipientIdentifier> r, long t) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendRemoteDeleteMessage(long t, Set<RecipientIdentifier> r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendMessageReaction(
|
||||
String e,
|
||||
boolean rm,
|
||||
RecipientIdentifier.Single a,
|
||||
long t,
|
||||
Set<RecipientIdentifier> r,
|
||||
boolean n,
|
||||
boolean s
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendAdminDelete(
|
||||
RecipientIdentifier.Single a,
|
||||
long t,
|
||||
Set<RecipientIdentifier.Group> r,
|
||||
boolean n,
|
||||
boolean s
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendPinMessage(
|
||||
int d,
|
||||
RecipientIdentifier.Single a,
|
||||
long t,
|
||||
Set<RecipientIdentifier> r,
|
||||
boolean n,
|
||||
boolean s
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendUnpinMessage(
|
||||
RecipientIdentifier.Single a,
|
||||
long t,
|
||||
Set<RecipientIdentifier> r,
|
||||
boolean n,
|
||||
boolean s
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendPaymentNotificationMessage(byte[] r, String n, RecipientIdentifier.Single re) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendMessageRequestResponse(
|
||||
MessageEnvelope.Sync.MessageRequestResponse.Type t,
|
||||
Set<RecipientIdentifier> r
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendPollCreateMessage(
|
||||
String q,
|
||||
boolean a,
|
||||
List<String> o,
|
||||
Set<RecipientIdentifier> r,
|
||||
boolean n
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendPollVoteMessage(
|
||||
RecipientIdentifier.Single a,
|
||||
long t,
|
||||
List<Integer> o,
|
||||
int v,
|
||||
Set<RecipientIdentifier> r,
|
||||
boolean n
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendPollTerminateMessage(long t, Set<RecipientIdentifier> r, boolean n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideRecipient(RecipientIdentifier.Single r) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRecipient(RecipientIdentifier.Single r) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteContact(RecipientIdentifier.Single r) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContactName(RecipientIdentifier.Single r, String g, String f, String ng, String nf, String n) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContactsBlocked(Collection<RecipientIdentifier.Single> r, boolean b) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGroupsBlocked(Collection<GroupId> g, boolean b) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExpirationTimer(RecipientIdentifier.Single r, int t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickerPackUrl uploadStickerPack(File p) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installStickerPack(StickerPackUrl u) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StickerPack> getStickerPacks() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestAllSyncData() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addReceiveHandler(ReceiveMessageHandler h, boolean w) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeReceiveHandler(ReceiveMessageHandler h) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReceiving() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveMessages(Optional<Duration> t, Optional<Integer> m, ReceiveMessageHandler h) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopReceiveMessages() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReceiveConfig(ReceiveConfig r) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContactBlocked(RecipientIdentifier.Single r) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendContacts() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Recipient> getRecipients(
|
||||
boolean o,
|
||||
Optional<Boolean> b,
|
||||
Collection<RecipientIdentifier.Single> a,
|
||||
Optional<String> n
|
||||
) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContactOrProfileName(RecipientIdentifier.Single r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Group getGroup(GroupId g) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identity> getIdentities() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identity> getIdentities(RecipientIdentifier.Single r) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trustIdentityVerified(RecipientIdentifier.Single r, IdentityVerificationCode v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trustIdentityAllKeys(RecipientIdentifier.Single r) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAddressChangedListener(Runnable l) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addClosedListener(Runnable l) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream retrieveAttachment(String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream retrieveContactAvatar(RecipientIdentifier.Single r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream retrieveProfileAvatar(RecipientIdentifier.Single r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream retrieveGroupAvatar(GroupId g) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream retrieveSticker(StickerPackId s, int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallInfo startCall(RecipientIdentifier.Single r) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallInfo acceptCall(long c) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hangupCall(long c) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResult rejectCall(long c) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallInfo> listActiveCalls() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCallOffer(RecipientIdentifier.Single r, CallOffer o) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCallAnswer(RecipientIdentifier.Single r, long c, byte[] a) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendIceUpdate(RecipientIdentifier.Single r, long c, List<byte[]> i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendHangup(RecipientIdentifier.Single r, long c, MessageEnvelope.Call.Hangup.Type t) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBusy(RecipientIdentifier.Single r, long c) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TurnServer> getTurnServerInfo() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal MultiAccountManager stub for multi-account mode tests.
|
||||
*/
|
||||
private static class StubMultiAccountManager implements MultiAccountManager {
|
||||
|
||||
final List<Manager> managers;
|
||||
final List<Consumer<Manager>> addedHandlers = new ArrayList<>();
|
||||
|
||||
@ -182,26 +587,48 @@ class SubscribeCallEventsTest {
|
||||
this.managers = new ArrayList<>(managers);
|
||||
}
|
||||
|
||||
@Override public List<String> getAccountNumbers() {
|
||||
@Override
|
||||
public List<String> getAccountNumbers() {
|
||||
return managers.stream().map(Manager::getSelfNumber).toList();
|
||||
}
|
||||
|
||||
@Override public List<Manager> getManagers() { return managers; }
|
||||
@Override
|
||||
public List<Manager> getManagers() {
|
||||
return managers;
|
||||
}
|
||||
|
||||
@Override public void addOnManagerAddedHandler(Consumer<Manager> handler) {
|
||||
@Override
|
||||
public void addOnManagerAddedHandler(Consumer<Manager> handler) {
|
||||
addedHandlers.add(handler);
|
||||
}
|
||||
|
||||
@Override public void addOnManagerRemovedHandler(Consumer<Manager> handler) {}
|
||||
@Override
|
||||
public void addOnManagerRemovedHandler(Consumer<Manager> handler) {
|
||||
}
|
||||
|
||||
@Override public Manager getManager(String phoneNumber) {
|
||||
@Override
|
||||
public Manager getManager(String phoneNumber) {
|
||||
return managers.stream().filter(m -> phoneNumber.equals(m.getSelfNumber())).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Override public URI getNewProvisioningDeviceLinkUri() { return null; }
|
||||
@Override public ProvisioningManager getProvisioningManagerFor(URI u) { return null; }
|
||||
@Override public RegistrationManager getNewRegistrationManager(String a) { return null; }
|
||||
@Override public void close() {}
|
||||
@Override
|
||||
public URI getNewProvisioningDeviceLinkUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProvisioningManager getProvisioningManagerFor(URI u) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrationManager getNewRegistrationManager(String a) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
|
||||
private static String jsonRpcCall(int id, String method) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user