This commit is contained in:
Scott Lewis 2025-02-17 15:05:55 -08:00
commit 19e7e1a493
17 changed files with 126 additions and 35 deletions

View file

@ -26,7 +26,7 @@ jobs:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v4
with:
dependency-graph: generate-and-submit
- name: Install asciidoc

View file

@ -2,6 +2,15 @@
## [Unreleased]
Requires libsignal-client version 0.65.6.
### Added
- Allow setting nickname and note with `updateContact` command
### Fixed
- Fix syncing nickname, note and expiration timer
- Fix check for registered users with a proxy
## [0.13.12] - 2025-01-18
Requires libsignal-client version 0.65.2.

View file

@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) .
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
So signal-cli releases older than three months may not work correctly.
## Installation
You can [build signal-cli](#building) yourself or use
@ -55,8 +59,15 @@ of all country codes.)
signal-cli -a ACCOUNT register
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
to the voice call verification by adding the `--voice` switch at the end of above register command.
You can register Signal using a landline number. In this case, you need to follow the procedure below:
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
* You will get an error `400 (InvalidTransportModeException)`, this is normal
* Wait 60 seconds
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
```sh
signal-cli -a ACCOUNT register --voice
```
Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)

View file

@ -3,7 +3,7 @@ plugins {
application
eclipse
`check-lib-versions`
id("org.graalvm.buildtools.native") version "0.10.4"
id("org.graalvm.buildtools.native") version "0.10.5"
}
allprojects {

View file

@ -413,7 +413,7 @@ pub enum CliCommands {
#[arg(long = "about-emoji")]
about_emoji: Option<String>,
#[arg(long = "mobile-coin-address")]
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
mobile_coin_address: Option<String>,
#[arg(long)]

View file

@ -88,7 +88,7 @@
},
{
"name":"org.signal.libsignal.internal.CompletableFuture",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }]
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }]
},
{
"name":"org.signal.libsignal.net.CdsiLookupResponse",
@ -110,6 +110,10 @@
{
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
},
{
"name":"org.signal.libsignal.net.NetworkException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]

View file

@ -188,6 +188,8 @@
"pattern":"\\Qlibsignal_jni_amd64.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni_amd64.so\\E"
}, {
"pattern":"\\Qlibsignal_jni_testing_amd64.so\\E"
}, {
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
}, {

View file

@ -10,8 +10,8 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.16"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_116"
sqlite = "org.xerial:sqlite-jdbc:3.48.0.0"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_117"
sqlite = "org.xerial:sqlite-jdbc:3.49.0.0"
hikari = "com.zaxxer:HikariCP:6.2.1"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.11.4"
junit-launcher = "org.junit.platform:junit-platform-launcher:1.11.4"

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View file

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View file

@ -210,12 +210,6 @@ public class StorageHelper {
remoteOnlyRecords.size());
}
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
if (!idDifference.localOnlyIds().isEmpty()) {
final var updated = account.getRecipientStore()
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
@ -228,6 +222,12 @@ public class StorageHelper {
}
}
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
final var unknownDeletes = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
logger.debug("Storage ids with unknown type: {} inserts, {} deletes",
unknownInserts.size(),
unknownDeletes.size());
@ -279,10 +279,22 @@ public class StorageHelper {
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
final var localStorageIds = getAllLocalStorageIds(connection);
final var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
var localStorageIds = getAllLocalStorageIds(connection);
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
logger.debug("ID Difference :: {}", idDifference);
final var unknownOnlyLocal = idDifference.localOnlyIds()
.stream()
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
.toList();
if (!unknownOnlyLocal.isEmpty()) {
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownOnlyLocal);
localStorageIds = getAllLocalStorageIds(connection);
idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
}
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
// TODO check if local storage record proto matches remote, then reset to remote storage_id
@ -595,7 +607,7 @@ public class StorageHelper {
final var remote = remoteByRawId.get(rawId);
final var local = localByRawId.get(rawId);
if (remote.getType() != local.getType() && local.getType() != 0) {
if (remote.getType() != local.getType() && KNOWN_TYPES.contains(local.getType())) {
remoteOnlyRawIds.remove(rawId);
localOnlyRawIds.remove(rawId);
hasTypeMismatch = true;

View file

@ -2,9 +2,12 @@ package org.asamk.signal.manager.internal;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -31,6 +34,9 @@ import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
@ -38,6 +44,8 @@ import java.util.function.Supplier;
public class SignalDependencies {
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
@ -129,8 +137,34 @@ public class SignalDependencies {
}
public Network getLibSignalNetwork() {
return getOrCreate(() -> libSignalNetwork,
() -> libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent));
return getOrCreate(() -> libSignalNetwork, () -> {
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
setSignalNetworkProxy(libSignalNetwork);
});
}
private void setSignalNetworkProxy(Network libSignalNetwork) {
final var proxy = Utils.getHttpsProxy();
if (proxy.address() instanceof InetSocketAddress addr) {
switch (proxy.type()) {
case Proxy.Type.DIRECT -> {
}
case Proxy.Type.HTTP -> {
try {
libSignalNetwork.setProxy("http", addr.getHostName(), addr.getPort(), null, null);
} catch (IOException e) {
logger.warn("Failed to set http proxy", e);
}
}
case Proxy.Type.SOCKS -> {
try {
libSignalNetwork.setProxy("socks", addr.getHostName(), addr.getPort(), null, null);
} catch (IOException e) {
logger.warn("Failed to set socks proxy", e);
}
}
}
}
}
public SignalServiceAccountManager getAccountManager() {

View file

@ -40,7 +40,7 @@ public class KeyValueStore {
try (final var connection = database.getConnection()) {
return getEntry(connection, key);
} catch (SQLException e) {
throw new RuntimeException("Failed read from pre_key store", e);
throw new RuntimeException("Failed read from key_value store", e);
}
}

View file

@ -47,38 +47,38 @@ public class NumberVerificationUtils {
}
}
sessionId = sessionResponse.getBody().getId();
sessionId = sessionResponse.getMetadata().getId();
sessionIdSaver.accept(sessionId);
if (sessionResponse.getBody().getVerified()) {
if (sessionResponse.getMetadata().getVerified()) {
return sessionId;
}
if (sessionResponse.getBody().getAllowedToRequestCode()) {
if (sessionResponse.getMetadata().getAllowedToRequestCode()) {
return sessionId;
}
final var nextAttempt = voiceVerification
? sessionResponse.getBody().getNextCall()
: sessionResponse.getBody().getNextSms();
? sessionResponse.getMetadata().getNextCall()
: sessionResponse.getMetadata().getNextSms();
if (nextAttempt == null) {
throw new VerificationMethodNotAvailableException();
} else if (nextAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000;
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
throw new RateLimitException(timestamp);
}
final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt();
final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000;
final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
throw new CaptchaRequiredException(timestamp);
}
if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) {
if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
if (captcha != null) {
sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
}
if (!sessionResponse.getBody().getAllowedToRequestCode()) {
if (!sessionResponse.getMetadata().getAllowedToRequestCode()) {
throw new CaptchaRequiredException("Captcha Required");
}
}

View file

@ -15,6 +15,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
@ -202,4 +206,19 @@ public class Utils {
public static String nullIfEmpty(String string) {
return string == null || string.isEmpty() ? null : string;
}
public static Proxy getHttpsProxy() {
final URI uri;
try {
uri = new URI("https://example");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
final var proxies = ProxySelector.getDefault().select(uri);
if (proxies.isEmpty()) {
return Proxy.NO_PROXY;
} else {
return proxies.getFirst();
}
}
}

View file

@ -657,7 +657,7 @@ Path to the new avatar image file.
*--remove-avatar*::
Remove the avatar
*--mobile-coin-address*::
*--mobile-coin-address*, **--mobilecoin-address**::
New MobileCoin address (Base64 encoded public address)
=== updateContact

View file

@ -27,7 +27,8 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand {
subparser.addArgument("--family-name").help("New profile family name (optional)");
subparser.addArgument("--about").help("New profile about text");
subparser.addArgument("--about-emoji").help("New profile about emoji");
subparser.addArgument("--mobile-coin-address").help("New MobileCoin address (Base64 encoded public address)");
subparser.addArgument("--mobile-coin-address", "--mobilecoin-address")
.help("New MobileCoin address (Base64 encoded public address)");
final var avatarOptions = subparser.addMutuallyExclusiveGroup();
avatarOptions.addArgument("--avatar").help("Path to new profile avatar");