mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
|
f6d81e3c05 | ||
|
42f10670b6 | ||
|
b453d7a0b9 | ||
|
f9a36c6e04 | ||
|
be48afb2b5 | ||
|
a0960fcabd | ||
|
dbc454ba9e | ||
|
2225e69277 | ||
|
783201d12e | ||
|
3e981d66e9 | ||
|
7c7fc76a64 | ||
|
c924d5c03a | ||
|
dc787be17b | ||
|
3d4070a139 | ||
|
dbdff83132 | ||
|
4ce194afe2 | ||
|
ca33249170 | ||
|
a96626c468 | ||
|
d54be747da | ||
|
ff846bc678 | ||
|
1b7f755590 | ||
|
887ed3bb44 | ||
|
3180eba836 | ||
|
cb06cbdcca | ||
|
069325af47 | ||
|
e7ca02f1fb |
38 changed files with 532 additions and 306 deletions
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
|
@ -69,3 +69,28 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: signal-cli-native
|
name: signal-cli-native
|
||||||
path: build/native/nativeCompile/signal-cli
|
path: build/native/nativeCompile/signal-cli
|
||||||
|
|
||||||
|
build-client:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu
|
||||||
|
- macos
|
||||||
|
- windows
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./client
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install rust
|
||||||
|
run: rustup default stable
|
||||||
|
- name: Build client
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: signal-cli-client-${{ matrix.os }}
|
||||||
|
path: |
|
||||||
|
client/target/release/signal-cli-client
|
||||||
|
client/target/release/signal-cli-client.exe
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -182,7 +182,7 @@ jobs:
|
||||||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||||
rm -r signal-cli-archive-* signal-cli-native
|
rm -r signal-cli-archive-* signal-cli-native
|
||||||
mkdir -p build/install/
|
mkdir -p build/install/
|
||||||
mv ./signal-cli-*/ build/install/signal-cli
|
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,5 +1,25 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.13.18] - 2025-07-16
|
||||||
|
|
||||||
|
Requires libsignal-client version 0.76.3.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `--view-once` parameter to send command to send view once images
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Handle rate limit exception correctly when querying usernames
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Shut down when dbus daemon connection goes away unexpectedly
|
||||||
|
- In daemon mode, exit immediately if account check fails at startup
|
||||||
|
- Improve behavior when sending to devices that have no available prekeys
|
||||||
|
|
||||||
## [0.13.17] - 2025-06-28
|
## [0.13.17] - 2025-06-28
|
||||||
|
|
||||||
Requires libsignal-client version 0.76.0.
|
Requires libsignal-client version 0.76.0.
|
||||||
|
|
|
@ -83,6 +83,12 @@ of all country codes.)
|
||||||
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* Send a message to a username, usernames need to be prefixed with `u:`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
|
||||||
|
```
|
||||||
|
|
||||||
* Pipe the message content from another process.
|
* Pipe the message content from another process.
|
||||||
|
|
||||||
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
||||||
|
|
|
@ -8,7 +8,7 @@ plugins {
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.asamk"
|
group = "org.asamk"
|
||||||
version = "0.13.17"
|
version = "0.13.19-SNAPSHOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
@file:Suppress("DEPRECATION")
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
import groovy.util.XmlSlurper
|
|
||||||
import groovy.util.slurpersupport.GPathResult
|
|
||||||
import org.codehaus.groovy.runtime.ResourceGroovyMethods
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.Task
|
import org.gradle.api.Task
|
||||||
import org.gradle.api.artifacts.Dependency
|
import org.gradle.api.artifacts.Dependency
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
|
@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
||||||
val name = dependency.name
|
val name = dependency.name
|
||||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
||||||
try {
|
try {
|
||||||
val url = ResourceGroovyMethods.toURL(metaDataUrl)
|
val dbf = DocumentBuilderFactory.newInstance()
|
||||||
val metaDataText = ResourceGroovyMethods.getText(url)
|
val db = dbf.newDocumentBuilder()
|
||||||
val metadata = XmlSlurper().parseText(metaDataText)
|
val doc = db.parse(metaDataUrl);
|
||||||
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
|
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||||
if (version != newest.toString()) {
|
if (version != newest.toString()) {
|
||||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||||
}
|
}
|
||||||
|
|
513
client/Cargo.lock
generated
513
client/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,17 @@
|
||||||
[package]
|
[package]
|
||||||
name = "signal-cli-client"
|
name = "signal-cli-client"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
||||||
log = "0.4"
|
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
||||||
jsonrpsee = { version = "0.24", features = [
|
jsonrpsee = { version = "0.25", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"async-client",
|
"async-client",
|
||||||
"http-client",
|
"http-client",
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub struct Cli {
|
||||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||||
|
|
||||||
/// UNIX socket address and port of signal-cli daemon
|
/// UNIX socket address and port of signal-cli daemon
|
||||||
|
#[cfg(unix)]
|
||||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||||
pub json_rpc_socket: Option<Option<OsString>>,
|
pub json_rpc_socket: Option<Option<OsString>>,
|
||||||
|
|
||||||
|
@ -84,6 +85,8 @@ pub enum CliCommands {
|
||||||
},
|
},
|
||||||
GetUserStatus {
|
GetUserStatus {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
username: Vec<String>,
|
||||||
},
|
},
|
||||||
JoinGroup {
|
JoinGroup {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
@ -176,6 +179,9 @@ pub enum CliCommands {
|
||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long)]
|
||||||
attachment: Vec<String>,
|
attachment: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
view_once: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
mention: Vec<String>,
|
mention: Vec<String>,
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,7 @@ pub trait Rpc {
|
||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "joinGroup", param_kind = map)]
|
#[method(name = "joinGroup", param_kind = map)]
|
||||||
|
@ -182,6 +183,7 @@ pub trait Rpc {
|
||||||
endSession: bool,
|
endSession: bool,
|
||||||
message: String,
|
message: String,
|
||||||
attachments: Vec<String>,
|
attachments: Vec<String>,
|
||||||
|
viewOnce: bool,
|
||||||
mentions: Vec<String>,
|
mentions: Vec<String>,
|
||||||
textStyle: Vec<String>,
|
textStyle: Vec<String>,
|
||||||
quoteTimestamp: Option<u64>,
|
quoteTimestamp: Option<u64>,
|
||||||
|
@ -190,10 +192,10 @@ pub trait Rpc {
|
||||||
quoteMention: Vec<String>,
|
quoteMention: Vec<String>,
|
||||||
quoteTextStyle: Vec<String>,
|
quoteTextStyle: Vec<String>,
|
||||||
quoteAttachment: Vec<String>,
|
quoteAttachment: Vec<String>,
|
||||||
preview_url: Option<String>,
|
previewUrl: Option<String>,
|
||||||
preview_title: Option<String>,
|
previewTitle: Option<String>,
|
||||||
preview_description: Option<String>,
|
previewDescription: Option<String>,
|
||||||
preview_image: Option<String>,
|
previewImage: Option<String>,
|
||||||
sticker: Option<String>,
|
sticker: Option<String>,
|
||||||
storyTimestamp: Option<u64>,
|
storyTimestamp: Option<u64>,
|
||||||
storyAuthor: Option<String>,
|
storyAuthor: Option<String>,
|
||||||
|
@ -409,6 +411,7 @@ pub async fn connect_tcp(
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
pub async fn connect_unix(
|
pub async fn connect_unix(
|
||||||
socket_path: impl AsRef<Path>,
|
socket_path: impl AsRef<Path>,
|
||||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||||
|
@ -417,6 +420,6 @@ pub async fn connect_unix(
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> {
|
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
|
||||||
HttpClientBuilder::default().build(uri)
|
HttpClientBuilder::default().build(uri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,13 @@ async fn handle_command(
|
||||||
.delete_local_account_data(cli.account, ignore_registered)
|
.delete_local_account_data(cli.account, ignore_registered)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::GetUserStatus { recipient } => {
|
CliCommands::GetUserStatus {
|
||||||
client.get_user_status(cli.account, recipient).await
|
recipient,
|
||||||
|
username,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.get_user_status(cli.account, recipient, username)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||||
CliCommands::Link { name } => {
|
CliCommands::Link { name } => {
|
||||||
|
@ -70,7 +75,7 @@ async fn handle_command(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||||
.device_link_uri;
|
.device_link_uri;
|
||||||
println!("{}", url);
|
println!("{url}");
|
||||||
client.finish_link(url, name).await
|
client.finish_link(url, name).await
|
||||||
}
|
}
|
||||||
CliCommands::ListAccounts => client.list_accounts().await,
|
CliCommands::ListAccounts => client.list_accounts().await,
|
||||||
|
@ -139,6 +144,7 @@ async fn handle_command(
|
||||||
end_session,
|
end_session,
|
||||||
message,
|
message,
|
||||||
attachment,
|
attachment,
|
||||||
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
text_style,
|
text_style,
|
||||||
quote_timestamp,
|
quote_timestamp,
|
||||||
|
@ -165,6 +171,7 @@ async fn handle_command(
|
||||||
end_session,
|
end_session,
|
||||||
message.unwrap_or_default(),
|
message.unwrap_or_default(),
|
||||||
attachment,
|
attachment,
|
||||||
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
text_style,
|
text_style,
|
||||||
quote_timestamp,
|
quote_timestamp,
|
||||||
|
@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
||||||
|
|
||||||
handle_command(cli, client).await
|
handle_command(cli, client).await
|
||||||
} else {
|
} else {
|
||||||
let socket_path = cli
|
#[cfg(windows)]
|
||||||
.json_rpc_socket
|
{
|
||||||
.clone()
|
Err(RpcError::Custom("Invalid socket".into()))
|
||||||
.unwrap_or(None)
|
}
|
||||||
.or_else(|| {
|
#[cfg(unix)]
|
||||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
{
|
||||||
PathBuf::from(runtime_dir)
|
let socket_path = cli
|
||||||
.join(DEFAULT_SOCKET_SUFFIX)
|
.json_rpc_socket
|
||||||
.into()
|
.clone()
|
||||||
|
.unwrap_or(None)
|
||||||
|
.or_else(|| {
|
||||||
|
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||||
|
PathBuf::from(runtime_dir)
|
||||||
|
.join(DEFAULT_SOCKET_SUFFIX)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
let client = jsonrpc::connect_unix(socket_path)
|
||||||
let client = jsonrpc::connect_unix(socket_path)
|
.await
|
||||||
.await
|
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
|
||||||
|
|
||||||
handle_command(cli, client).await
|
handle_command(cli, client).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||||
use jsonrpsee::core::{
|
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||||
async_trait,
|
|
||||||
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
mod stream_codec;
|
mod stream_codec;
|
||||||
pub mod tcp;
|
pub mod tcp;
|
||||||
|
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
|
||||||
inner: T,
|
inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||||
for Sender<T>
|
for Sender<T>
|
||||||
{
|
{
|
||||||
|
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
||||||
self.inner
|
self.inner
|
||||||
.send(body)
|
.send(body)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
||||||
self.inner
|
self.inner
|
||||||
.close()
|
.close()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
|
||||||
inner: T,
|
inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||||
for Receiver<T>
|
for Receiver<T>
|
||||||
{
|
{
|
||||||
|
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
|
||||||
match self.inner.next().await {
|
match self.inner.next().await {
|
||||||
None => Err(Errors::Closed),
|
None => Err(Errors::Closed),
|
||||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||||
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
|
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
|
||||||
|
|
||||||
match str::from_utf8(line.as_ref()) {
|
match str::from_utf8(line.as_ref()) {
|
||||||
Ok(s) => Ok(Some(s.to_string())),
|
Ok(s) => Ok(Some(s.to_string())),
|
||||||
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")),
|
Err(_) => Err(io::Error::other("invalid UTF-8")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
|
@ -45,6 +45,9 @@
|
||||||
<content_attribute id="social-chat">intense</content_attribute>
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
</content_rating>
|
</content_rating>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.13.18" date="2025-07-16">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
|
||||||
|
</release>
|
||||||
<release version="0.13.17" date="2025-06-28">
|
<release version="0.13.17" date="2025-06-28">
|
||||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
|
||||||
</release>
|
</release>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"java.lang.Throwable",
|
"name":"java.lang.Throwable",
|
||||||
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
|
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"java.lang.UnsatisfiedLinkError",
|
"name":"java.lang.UnsatisfiedLinkError",
|
||||||
|
@ -126,6 +126,10 @@
|
||||||
"name":"org.signal.libsignal.net.NetworkException",
|
"name":"org.signal.libsignal.net.NetworkException",
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.signal.libsignal.net.RetryLaterException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
|
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
|
|
@ -3070,6 +3070,7 @@
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
|
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
"fields":[{"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}],
|
||||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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" }
|
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||||
logback = "ch.qos.logback:logback-classic:1.5.18"
|
logback = "ch.qos.logback:logback-classic:1.5.18"
|
||||||
|
|
||||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_126"
|
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
|
||||||
sqlite = "org.xerial:sqlite-jdbc:3.50.1.0"
|
sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
|
||||||
hikari = "com.zaxxer:HikariCP:6.3.0"
|
hikari = "com.zaxxer:HikariCP:6.3.0"
|
||||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
|
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
|
||||||
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"
|
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
2
gradlew
vendored
2
gradlew
vendored
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -96,7 +96,7 @@ public interface Manager extends Closeable {
|
||||||
*/
|
*/
|
||||||
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
|
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
|
||||||
|
|
||||||
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames);
|
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
|
||||||
|
|
||||||
void updateAccountAttributes(
|
void updateAccountAttributes(
|
||||||
String deviceName,
|
String deviceName,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AccountCheckException;
|
import org.asamk.signal.manager.api.AccountCheckException;
|
||||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||||
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
|
@ -63,19 +64,28 @@ public class SignalAccountFiles {
|
||||||
return accountsStore.getAllNumbers();
|
return accountsStore.getAllNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
|
||||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||||
try {
|
try {
|
||||||
return initManager(a.number(), a.path());
|
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||||
} catch (NotRegisteredException | IOException | AccountCheckException e) {
|
} catch (NotRegisteredException e) {
|
||||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
return null;
|
return null;
|
||||||
} catch (Throwable e) {
|
} catch (AccountCheckException | IOException e) {
|
||||||
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
throw e;
|
return new Pair<Manager, Throwable>(null, e);
|
||||||
}
|
}
|
||||||
}).filter(Objects::nonNull).toList();
|
}).filter(Objects::nonNull).toList();
|
||||||
|
|
||||||
|
for (final var pair : managerPairs) {
|
||||||
|
if (pair.second() instanceof IOException e) {
|
||||||
|
throw e;
|
||||||
|
} else if (pair.second() instanceof AccountCheckException e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var managers = managerPairs.stream().map(Pair::first).toList();
|
||||||
return new MultiAccountManagerImpl(managers, this);
|
return new MultiAccountManagerImpl(managers, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Optional;
|
||||||
public record Message(
|
public record Message(
|
||||||
String messageText,
|
String messageText,
|
||||||
List<String> attachments,
|
List<String> attachments,
|
||||||
|
boolean viewOnce,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
Optional<Quote> quote,
|
Optional<Quote> quote,
|
||||||
Optional<Sticker> sticker,
|
Optional<Sticker> sticker,
|
||||||
|
|
|
@ -28,8 +28,9 @@ class LiveConfig {
|
||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||||
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
|
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
|
||||||
private static final String SVR2_MRENCLAVE = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
|
private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
|
||||||
|
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
|
||||||
|
|
||||||
private static final String URL = "https://chat.signal.org";
|
private static final String URL = "https://chat.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn.signal.org";
|
private static final String CDN_URL = "https://cdn.signal.org";
|
||||||
|
@ -91,7 +92,7 @@ class LiveConfig {
|
||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoot(),
|
getUnidentifiedSenderTrustRoot(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveConfig() {
|
private LiveConfig() {
|
||||||
|
|
|
@ -28,8 +28,9 @@ class StagingConfig {
|
||||||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||||
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
||||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
private static final String SVR2_MRENCLAVE_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
|
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
|
||||||
private static final String SVR2_MRENCLAVE = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
|
private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
|
||||||
|
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
|
||||||
|
|
||||||
private static final String URL = "https://chat.staging.signal.org";
|
private static final String URL = "https://chat.staging.signal.org";
|
||||||
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||||
|
@ -91,7 +92,7 @@ class StagingConfig {
|
||||||
createDefaultServiceConfiguration(interceptors),
|
createDefaultServiceConfiguration(interceptors),
|
||||||
getUnidentifiedSenderTrustRoot(),
|
getUnidentifiedSenderTrustRoot(),
|
||||||
CDSI_MRENCLAVE,
|
CDSI_MRENCLAVE,
|
||||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StagingConfig() {
|
private StagingConfig() {
|
||||||
|
|
|
@ -41,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
||||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||||
import org.signal.libsignal.metadata.SelfSendException;
|
import org.signal.libsignal.metadata.SelfSendException;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
|
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
@ -105,7 +106,7 @@ public final class IncomingMessageHandler {
|
||||||
try {
|
try {
|
||||||
final var cipherResult = dependencies.getCipher(destination == null
|
final var cipherResult = dependencies.getCipher(destination == null
|
||||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
||||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return new Pair<>(List.of(), null);
|
return new Pair<>(List.of(), null);
|
||||||
|
@ -143,7 +144,7 @@ public final class IncomingMessageHandler {
|
||||||
try {
|
try {
|
||||||
final var cipherResult = dependencies.getCipher(destination == null
|
final var cipherResult = dependencies.getCipher(destination == null
|
||||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
||||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return new Pair<>(List.of(), null);
|
return new Pair<>(List.of(), null);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -68,7 +69,7 @@ public class RecipientHelper {
|
||||||
.toSignalServiceAddress();
|
.toSignalServiceAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
|
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
|
||||||
final var recipientIds = new HashSet<RecipientId>(recipients.size());
|
final var recipientIds = new HashSet<RecipientId>(recipients.size());
|
||||||
for (var number : recipients) {
|
for (var number : recipients) {
|
||||||
final var recipientId = resolveRecipient(number);
|
final var recipientId = resolveRecipient(number);
|
||||||
|
@ -91,7 +92,11 @@ public class RecipientHelper {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (recipient instanceof RecipientIdentifier.Username(String username)) {
|
} else if (recipient instanceof RecipientIdentifier.Username(String username)) {
|
||||||
return resolveRecipientByUsernameOrLink(username, false);
|
try {
|
||||||
|
return resolveRecipientByUsernameOrLink(username, false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
|
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
|
||||||
}
|
}
|
||||||
|
@ -99,7 +104,7 @@ public class RecipientHelper {
|
||||||
public RecipientId resolveRecipientByUsernameOrLink(
|
public RecipientId resolveRecipientByUsernameOrLink(
|
||||||
String username,
|
String username,
|
||||||
boolean forceRefresh
|
boolean forceRefresh
|
||||||
) throws UnregisteredRecipientException {
|
) throws UnregisteredRecipientException, IOException {
|
||||||
final Username finalUsername;
|
final Username finalUsername;
|
||||||
try {
|
try {
|
||||||
finalUsername = getUsernameFromUsernameOrLink(username);
|
finalUsername = getUsernameFromUsernameOrLink(username);
|
||||||
|
@ -110,11 +115,15 @@ public class RecipientHelper {
|
||||||
try {
|
try {
|
||||||
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||||
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
||||||
} catch (IOException e) {
|
} catch (NonSuccessfulResponseCodeException e) {
|
||||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
if (e.code == 404) {
|
||||||
null,
|
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||||
null,
|
null,
|
||||||
username));
|
null,
|
||||||
|
username));
|
||||||
|
}
|
||||||
|
logger.debug("Failed to get uuid for username: {}", username, e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
||||||
|
|
|
@ -285,7 +285,7 @@ public class ManagerImpl implements Manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) {
|
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
|
||||||
final var registeredUsers = new HashMap<String, RecipientAddress>();
|
final var registeredUsers = new HashMap<String, RecipientAddress>();
|
||||||
for (final var username : usernames) {
|
for (final var username : usernames) {
|
||||||
try {
|
try {
|
||||||
|
@ -810,6 +810,7 @@ public class ManagerImpl implements Manager {
|
||||||
} else if (!additionalAttachments.isEmpty()) {
|
} else if (!additionalAttachments.isEmpty()) {
|
||||||
messageBuilder.withAttachments(additionalAttachments);
|
messageBuilder.withAttachments(additionalAttachments);
|
||||||
}
|
}
|
||||||
|
messageBuilder.withViewOnce(message.viewOnce());
|
||||||
if (!message.mentions().isEmpty()) {
|
if (!message.mentions().isEmpty()) {
|
||||||
messageBuilder.withMentions(resolveMentions(message.mentions()));
|
messageBuilder.withMentions(resolveMentions(message.mentions()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
|
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -326,7 +327,8 @@ public class SignalDependencies {
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
executor,
|
executor,
|
||||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||||
() -> true));
|
() -> true,
|
||||||
|
UsePqRatchet.NO));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
|
private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
|
||||||
|
|
||||||
private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$");
|
private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{6,18}$");
|
||||||
|
|
||||||
private final ACI selfAci;
|
private final ACI selfAci;
|
||||||
private final PNI selfPni;
|
private final PNI selfPni;
|
||||||
|
|
|
@ -316,6 +316,11 @@ Data URI encoded attachments must follow the RFC 2397.
|
||||||
Additionally a file name can be added:
|
Additionally a file name can be added:
|
||||||
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
|
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
|
||||||
|
|
||||||
|
*--view-once*::
|
||||||
|
Send the message as a view once message.
|
||||||
|
A conformant client will only allow the receiver to view the message once.
|
||||||
|
View Once is only supported for messages that include an image attachment.
|
||||||
|
|
||||||
*--sticker* STICKER::
|
*--sticker* STICKER::
|
||||||
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
|
Send a sticker of a locally known sticker pack (syntax: stickerPackId:stickerId).
|
||||||
Shouldn't be used together with `-m` as the official clients don't support this.
|
Shouldn't be used together with `-m` as the official clients don't support this.
|
||||||
|
|
|
@ -291,6 +291,8 @@ public class App {
|
||||||
commandHandler.handleMultiLocalCommand(command, multiAccountManager);
|
commandHandler.handleMultiLocalCommand(command, multiAccountManager);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IOErrorException("Failed to load local accounts file", e);
|
throw new IOErrorException("Failed to load local accounts file", e);
|
||||||
|
} catch (AccountCheckException e) {
|
||||||
|
throw new UnexpectedErrorException("Failed to load account file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.asamk.signal;
|
package org.asamk.signal;
|
||||||
|
|
||||||
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ import sun.misc.Signal;
|
||||||
public class Shutdown {
|
public class Shutdown {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(Shutdown.class);
|
private static final Logger logger = LoggerFactory.getLogger(Shutdown.class);
|
||||||
private static final CompletableFuture<Void> shutdown = new CompletableFuture<>();
|
private static final CompletableFuture<Object> shutdown = new CompletableFuture<>();
|
||||||
private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>();
|
private static final CompletableFuture<Void> shutdownComplete = new CompletableFuture<>();
|
||||||
private static boolean initialized = false;
|
private static boolean initialized = false;
|
||||||
|
|
||||||
|
@ -43,9 +44,17 @@ public class Shutdown {
|
||||||
shutdown.complete(null);
|
shutdown.complete(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void waitForShutdown() throws InterruptedException {
|
public static void triggerShutdown(CommandException exception) {
|
||||||
|
logger.debug("Triggering shutdown with exception.", exception);
|
||||||
|
shutdown.complete(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void waitForShutdown() throws InterruptedException, CommandException {
|
||||||
try {
|
try {
|
||||||
shutdown.get();
|
final var result = shutdown.get();
|
||||||
|
if (result instanceof CommandException e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ public class DaemonCommand implements MultiLocalCommand, LocalCommand {
|
||||||
final OutputWriter outputWriter
|
final OutputWriter outputWriter
|
||||||
) throws CommandException {
|
) throws CommandException {
|
||||||
Shutdown.installHandler();
|
Shutdown.installHandler();
|
||||||
logger.info("Starting daemon in single-account mode for " + m.getSelfNumber());
|
logger.info("Starting daemon in single-account mode for {}", m.getSelfNumber());
|
||||||
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
|
final var noReceiveStdOut = Boolean.TRUE.equals(ns.getBoolean("no-receive-stdout"));
|
||||||
final var receiveMode = ns.<ReceiveMode>get("receive-mode");
|
final var receiveMode = ns.<ReceiveMode>get("receive-mode");
|
||||||
final var receiveConfig = getReceiveConfig(ns);
|
final var receiveConfig = getReceiveConfig(ns);
|
||||||
|
|
|
@ -64,9 +64,16 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
final var usernames = ns.<String>getList("username");
|
final var usernames = ns.<String>getList("username");
|
||||||
final var registeredUsernames = usernames == null
|
final Map<String, UsernameStatus> registeredUsernames;
|
||||||
? Map.<String, UsernameStatus>of()
|
try {
|
||||||
: m.getUsernameStatus(new HashSet<>(usernames));
|
registeredUsernames = usernames == null ? Map.of() : m.getUsernameStatus(new HashSet<>(usernames));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Unable to check if users are registered: "
|
||||||
|
+ e.getMessage()
|
||||||
|
+ " ("
|
||||||
|
+ e.getClass().getSimpleName()
|
||||||
|
+ ")", e);
|
||||||
|
}
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
switch (outputWriter) {
|
switch (outputWriter) {
|
||||||
|
|
|
@ -66,6 +66,9 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||||
.help("Add an attachment. "
|
.help("Add an attachment. "
|
||||||
+ "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. "
|
+ "Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Additionally a file name can be added, e.g. "
|
||||||
+ "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
|
+ "data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>.");
|
||||||
|
subparser.addArgument("--view-once")
|
||||||
|
.action(Arguments.storeTrue())
|
||||||
|
.help("Send the message as a view once message");
|
||||||
subparser.addArgument("-e", "--end-session", "--endsession")
|
subparser.addArgument("-e", "--end-session", "--endsession")
|
||||||
.help("Clear session state and send end session message.")
|
.help("Clear session state and send end session message.")
|
||||||
.action(Arguments.storeTrue());
|
.action(Arguments.storeTrue());
|
||||||
|
@ -164,6 +167,7 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||||
if (attachments == null) {
|
if (attachments == null) {
|
||||||
attachments = List.of();
|
attachments = List.of();
|
||||||
}
|
}
|
||||||
|
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
|
||||||
|
|
||||||
final var selfNumber = m.getSelfNumber();
|
final var selfNumber = m.getSelfNumber();
|
||||||
|
|
||||||
|
@ -239,6 +243,7 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
|
viewOnce,
|
||||||
mentions,
|
mentions,
|
||||||
Optional.ofNullable(quote),
|
Optional.ofNullable(quote),
|
||||||
Optional.ofNullable(sticker),
|
Optional.ofNullable(sticker),
|
||||||
|
@ -250,8 +255,14 @@ public class SendCommand implements JsonRpcLocalCommand {
|
||||||
: m.sendMessage(message, recipientIdentifiers, notifySelf);
|
: m.sendMessage(message, recipientIdentifiers, notifySelf);
|
||||||
outputResult(outputWriter, results);
|
outputResult(outputWriter, results);
|
||||||
} catch (AttachmentInvalidException | IOException e) {
|
} catch (AttachmentInvalidException | IOException e) {
|
||||||
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
|
if (e instanceof IOException io && io.getMessage().contains("No prekeys available")) {
|
||||||
.getSimpleName() + ")", e);
|
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
|
||||||
|
.getSimpleName() + "), maybe one of the devices of the recipient wasn't online for a while.",
|
||||||
|
e);
|
||||||
|
} else {
|
||||||
|
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
|
||||||
|
.getSimpleName() + ")", e);
|
||||||
|
}
|
||||||
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
|
||||||
throw new UserErrorException(e.getMessage());
|
throw new UserErrorException(e.getMessage());
|
||||||
} catch (UnregisteredRecipientException e) {
|
} catch (UnregisteredRecipientException e) {
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
package org.asamk.signal.dbus;
|
package org.asamk.signal.dbus;
|
||||||
|
|
||||||
import org.asamk.signal.DbusConfig;
|
import org.asamk.signal.DbusConfig;
|
||||||
|
import org.asamk.signal.Shutdown;
|
||||||
import org.asamk.signal.commands.exceptions.CommandException;
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
|
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||||
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
|
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
|
||||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
import org.asamk.signal.manager.Manager;
|
import org.asamk.signal.manager.Manager;
|
||||||
import org.asamk.signal.manager.MultiAccountManager;
|
import org.asamk.signal.manager.MultiAccountManager;
|
||||||
|
import org.freedesktop.dbus.connections.IDisconnectCallback;
|
||||||
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
||||||
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
|
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
|
||||||
import org.freedesktop.dbus.exceptions.DBusException;
|
import org.freedesktop.dbus.exceptions.DBusException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -94,7 +98,9 @@ public class DbusHandler implements AutoCloseable {
|
||||||
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
|
final var busType = isDbusSystem ? DBusConnection.DBusBusType.SYSTEM : DBusConnection.DBusBusType.SESSION;
|
||||||
logger.debug("Starting DBus server on {} bus: {}", busType, busname);
|
logger.debug("Starting DBus server on {} bus: {}", busType, busname);
|
||||||
try {
|
try {
|
||||||
dBusConnection = DBusConnectionBuilder.forType(busType).build();
|
dBusConnection = DBusConnectionBuilder.forType(busType)
|
||||||
|
.withDisconnectCallback(new DisconnectCallback())
|
||||||
|
.build();
|
||||||
dbusRunner.run(dBusConnection);
|
dbusRunner.run(dBusConnection);
|
||||||
} catch (DBusException e) {
|
} catch (DBusException e) {
|
||||||
throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e);
|
throw new UnexpectedErrorException("Dbus command failed: " + e.getMessage(), e);
|
||||||
|
@ -141,4 +147,13 @@ public class DbusHandler implements AutoCloseable {
|
||||||
|
|
||||||
void run(DBusConnection connection) throws DBusException;
|
void run(DBusConnection connection) throws DBusException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class DisconnectCallback implements IDisconnectCallback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnectOnError(IOException ex) {
|
||||||
|
logger.debug("DBus daemon disconnected unexpectedly, shutting down");
|
||||||
|
Shutdown.triggerShutdown(new IOErrorException("Unexpected dbus daemon disconnect", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,6 +236,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
@ -399,6 +400,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
@ -444,6 +446,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
||||||
try {
|
try {
|
||||||
final var message = new Message(messageText,
|
final var message = new Message(messageText,
|
||||||
attachments,
|
attachments,
|
||||||
|
false,
|
||||||
List.of(),
|
List.of(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue