mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-28 18:10:38 +00:00
Merge branch 'master' of https://github.com/scottslewis/signal-cli.git
This commit is contained in:
commit
19e7e1a493
17 changed files with 126 additions and 35 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
15
README.md
15
README.md
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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"] }]
|
||||
|
|
|
@ -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"
|
||||
}, {
|
||||
|
|
|
@ -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"
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
3
gradlew
vendored
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue