mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
parent
edbf803a98
commit
8037fb2d66
10 changed files with 928 additions and 521 deletions
|
@ -10,13 +10,17 @@ pub struct Cli {
|
|||
pub account: Option<String>,
|
||||
|
||||
/// TCP host and port of signal-cli daemon
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "json_rpc_http")]
|
||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||
|
||||
/// UNIX socket address and port of signal-cli daemon
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||
pub json_rpc_socket: Option<Option<OsString>>,
|
||||
|
||||
/// HTTP URL of signal-cli daemon
|
||||
#[arg(long, conflicts_with = "json_rpc_socket")]
|
||||
pub json_rpc_http: Option<Option<String>>,
|
||||
|
||||
#[arg(value_enum, long, default_value_t = OutputTypes::Json)]
|
||||
pub output: OutputTypes,
|
||||
|
||||
|
|
|
@ -1,49 +1,59 @@
|
|||
use std::path::Path;
|
||||
|
||||
use jsonrpc_client_transports::{transports::ipc, RpcError};
|
||||
use jsonrpc_core::serde::Deserialize;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpsee::async_client::ClientBuilder;
|
||||
use jsonrpsee::core::client::SubscriptionClientT;
|
||||
use jsonrpsee::core::Error;
|
||||
use jsonrpsee::http_client::HttpClientBuilder;
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
pub type SignalCliClient = gen_client::Client;
|
||||
|
||||
#[rpc(client, params = "named")]
|
||||
#[rpc(client)]
|
||||
pub trait Rpc {
|
||||
#[rpc(name = "addDevice", params = "named")]
|
||||
fn add_device(&self, account: Option<String>, uri: String) -> Result<Value>;
|
||||
#[method(name = "addDevice", param_kind = map)]
|
||||
async fn add_device(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
uri: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "block", params = "named")]
|
||||
#[method(name = "block", param_kind = map)]
|
||||
fn block(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "deleteLocalAccountData", params = "named")]
|
||||
#[method(name = "deleteLocalAccountData", param_kind = map)]
|
||||
fn delete_local_account_data(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "getUserStatus", params = "named")]
|
||||
fn get_user_status(&self, account: Option<String>, recipients: Vec<String>) -> Result<Value>;
|
||||
#[method(name = "getUserStatus", param_kind = map)]
|
||||
fn get_user_status(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "joinGroup", params = "named")]
|
||||
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value>;
|
||||
#[method(name = "joinGroup", param_kind = map)]
|
||||
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "finishLink", params = "named")]
|
||||
#[method(name = "finishLink", param_kind = map)]
|
||||
fn finish_link(
|
||||
&self,
|
||||
#[allow(non_snake_case)] deviceLinkUri: String,
|
||||
#[allow(non_snake_case)] deviceName: String,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listAccounts", params = "named")]
|
||||
fn list_accounts(&self) -> Result<Value>;
|
||||
#[method(name = "listAccounts", param_kind = map)]
|
||||
fn list_accounts(&self) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listContacts", params = "named")]
|
||||
#[method(name = "listContacts", param_kind = map)]
|
||||
fn list_contacts(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -51,60 +61,64 @@ pub trait Rpc {
|
|||
#[allow(non_snake_case)] allRecipients: bool,
|
||||
blocked: Option<bool>,
|
||||
name: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listDevices", params = "named")]
|
||||
fn list_devices(&self, account: Option<String>) -> Result<Value>;
|
||||
#[method(name = "listDevices", param_kind = map)]
|
||||
fn list_devices(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listGroups", params = "named")]
|
||||
#[method(name = "listGroups", param_kind = map)]
|
||||
fn list_groups(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listIdentities", params = "named")]
|
||||
fn list_identities(&self, account: Option<String>, number: Option<String>) -> Result<Value>;
|
||||
#[method(name = "listIdentities", param_kind = map)]
|
||||
fn list_identities(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
number: Option<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "listStickerPacks", params = "named")]
|
||||
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value>;
|
||||
#[method(name = "listStickerPacks", param_kind = map)]
|
||||
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "quitGroup", params = "named")]
|
||||
#[method(name = "quitGroup", param_kind = map)]
|
||||
fn quit_group(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] groupId: String,
|
||||
delete: bool,
|
||||
admins: Vec<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "register", params = "named")]
|
||||
#[method(name = "register", param_kind = map)]
|
||||
fn register(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
voice: bool,
|
||||
captcha: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "removeContact", params = "named")]
|
||||
#[method(name = "removeContact", param_kind = map)]
|
||||
fn remove_contact(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
forget: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "removeDevice", params = "named")]
|
||||
#[method(name = "removeDevice", param_kind = map)]
|
||||
fn remove_device(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] deviceId: u32,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "removePin", params = "named")]
|
||||
fn remove_pin(&self, account: Option<String>) -> Result<Value>;
|
||||
#[method(name = "removePin", param_kind = map)]
|
||||
fn remove_pin(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "remoteDelete", params = "named")]
|
||||
#[method(name = "remoteDelete", param_kind = map)]
|
||||
fn remote_delete(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -112,9 +126,9 @@ pub trait Rpc {
|
|||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
#[allow(non_snake_case)] noteToSelf: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "send", params = "named")]
|
||||
#[method(name = "send", param_kind = map)]
|
||||
fn send(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -132,21 +146,21 @@ pub trait Rpc {
|
|||
sticker: Option<String>,
|
||||
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
||||
#[allow(non_snake_case)] storyAuthor: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendContacts", params = "named")]
|
||||
fn send_contacts(&self, account: Option<String>) -> Result<Value>;
|
||||
#[method(name = "sendContacts", param_kind = map)]
|
||||
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendPaymentNotification", params = "named")]
|
||||
#[method(name = "sendPaymentNotification", param_kind = map)]
|
||||
fn send_payment_notification(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
receipt: String,
|
||||
note: String,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendReaction", params = "named")]
|
||||
#[method(name = "sendReaction", param_kind = map)]
|
||||
fn send_reaction(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -158,75 +172,75 @@ pub trait Rpc {
|
|||
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||
remove: bool,
|
||||
story: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendReceipt", params = "named")]
|
||||
#[method(name = "sendReceipt", param_kind = map)]
|
||||
fn send_receipt(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
||||
r#type: String,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendSyncRequest", params = "named")]
|
||||
fn send_sync_request(&self, account: Option<String>) -> Result<Value>;
|
||||
#[method(name = "sendSyncRequest", param_kind = map)]
|
||||
fn send_sync_request(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "sendTyping", params = "named")]
|
||||
#[method(name = "sendTyping", param_kind = map)]
|
||||
fn send_typing(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
stop: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "setPin", params = "named")]
|
||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value>;
|
||||
#[method(name = "setPin", param_kind = map)]
|
||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "submitRateLimitChallenge", params = "named")]
|
||||
#[method(name = "submitRateLimitChallenge", param_kind = map)]
|
||||
fn submit_rate_limit_challenge(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
challenge: String,
|
||||
captcha: String,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "startLink", params = "named")]
|
||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink>;
|
||||
#[method(name = "startLink", param_kind = map)]
|
||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "trust", params = "named")]
|
||||
#[method(name = "trust", param_kind = map)]
|
||||
fn trust(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
#[allow(non_snake_case)] trustAllKnownKeys: bool,
|
||||
#[allow(non_snake_case)] verifiedSafetyNumber: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "unblock", params = "named")]
|
||||
#[method(name = "unblock", param_kind = map)]
|
||||
fn unblock(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "unregister", params = "named")]
|
||||
#[method(name = "unregister", param_kind = map)]
|
||||
fn unregister(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] deleteAccount: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "updateAccount", params = "named")]
|
||||
#[method(name = "updateAccount", param_kind = map)]
|
||||
fn update_account(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] deviceName: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "updateConfiguration", params = "named")]
|
||||
#[method(name = "updateConfiguration", param_kind = map)]
|
||||
fn update_configuration(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -234,18 +248,18 @@ pub trait Rpc {
|
|||
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
|
||||
#[allow(non_snake_case)] typingIndicators: Option<bool>,
|
||||
#[allow(non_snake_case)] linkPreviews: Option<bool>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "updateContact", params = "named")]
|
||||
#[method(name = "updateContact", param_kind = map)]
|
||||
fn update_contact(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
recipient: String,
|
||||
name: Option<String>,
|
||||
expiration: Option<u32>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "updateGroup", params = "named")]
|
||||
#[method(name = "updateGroup", param_kind = map)]
|
||||
fn update_group(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -265,9 +279,9 @@ pub trait Rpc {
|
|||
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
||||
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
||||
expiration: Option<u32>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "updateProfile", params = "named")]
|
||||
#[method(name = "updateProfile", param_kind = map)]
|
||||
fn update_profile(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
|
@ -278,32 +292,33 @@ pub trait Rpc {
|
|||
#[allow(non_snake_case)] mobileCoinAddress: Option<String>,
|
||||
avatar: Option<String>,
|
||||
#[allow(non_snake_case)] removeAvatar: bool,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "uploadStickerPack", params = "named")]
|
||||
fn upload_sticker_pack(&self, account: Option<String>, path: String) -> Result<Value>;
|
||||
#[method(name = "uploadStickerPack", param_kind = map)]
|
||||
fn upload_sticker_pack(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
path: String,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[rpc(name = "verify", params = "named")]
|
||||
#[method(name = "verify", param_kind = map)]
|
||||
fn verify(
|
||||
&self,
|
||||
account: Option<String>,
|
||||
#[allow(non_snake_case)] verificationCode: String,
|
||||
pin: Option<String>,
|
||||
) -> Result<Value>;
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[pubsub(
|
||||
subscription = "receive",
|
||||
subscribe,
|
||||
name = "subscribeReceive",
|
||||
params = "named"
|
||||
#[subscription(
|
||||
name = "subscribeReceive" => "receive",
|
||||
unsubscribe = "unsubscribeReceive",
|
||||
item = Value,
|
||||
param_kind = map
|
||||
)]
|
||||
fn subscribe_receive(&self, _: Self::Metadata, _: Subscriber<Value>, account: Option<String>);
|
||||
async fn subscribe_receive(&self, account: Option<String>) -> SubscriptionResult;
|
||||
|
||||
#[pubsub(subscription = "receive", unsubscribe, name = "unsubscribeReceive")]
|
||||
fn unsubscribe_receive(&self, _: Option<Self::Metadata>, _: SubscriptionId) -> Result<bool>;
|
||||
|
||||
#[rpc(name = "version")]
|
||||
fn version(&self) -> Result<Value>;
|
||||
#[method(name = "version")]
|
||||
fn version(&self) -> Result<Value, ErrorObjectOwned>;
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -312,10 +327,20 @@ pub struct JsonLink {
|
|||
pub device_link_uri: String,
|
||||
}
|
||||
|
||||
pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<SignalCliClient, RpcError> {
|
||||
super::tcp::connect::<_, SignalCliClient>(tcp).await
|
||||
pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<impl SubscriptionClientT, Error> {
|
||||
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
|
||||
|
||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
pub async fn connect_unix(socket_path: impl AsRef<Path>) -> Result<SignalCliClient, RpcError> {
|
||||
ipc::connect::<_, SignalCliClient>(socket_path).await
|
||||
pub async fn connect_unix(
|
||||
socket_path: impl AsRef<Path>,
|
||||
) -> Result<impl SubscriptionClientT, Error> {
|
||||
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
|
||||
|
||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> {
|
||||
HttpClientBuilder::default().build(uri)
|
||||
}
|
||||
|
|
|
@ -1,40 +1,53 @@
|
|||
use clap::Parser;
|
||||
use jsonrpc_client_transports::{RpcError, TypedSubscriptionStream};
|
||||
use jsonrpc_core::{futures_util::StreamExt, Value};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use cli::Cli;
|
||||
use jsonrpsee::core::client::{Subscription, SubscriptionClientT};
|
||||
use jsonrpsee::core::Error as RpcError;
|
||||
use serde_json::Value;
|
||||
use tokio::{select, time::sleep};
|
||||
|
||||
use crate::cli::{GroupPermission, LinkState};
|
||||
use crate::jsonrpc::RpcClient;
|
||||
|
||||
mod cli;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
||||
mod jsonrpc;
|
||||
mod tcp;
|
||||
mod transports;
|
||||
|
||||
const DEFAULT_TCP: &str = "127.0.0.1:7583";
|
||||
const DEFAULT_SOCKET_SUFFIX: &str = "signal-cli/socket";
|
||||
const DEFAULT_HTTP: &str = "http://localhost:8080/api/v1/rpc";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
let client = connect(&cli)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to socket: {e}"))?;
|
||||
let result = connect(cli).await;
|
||||
|
||||
let result = match cli.command {
|
||||
match result {
|
||||
Ok(Value::Null) => {}
|
||||
Ok(v) => println!("{v}"),
|
||||
Err(e) => return Err(anyhow::anyhow!("JSON-RPC command failed: {e:?}")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_command(
|
||||
cli: Cli,
|
||||
client: impl SubscriptionClientT + Sync,
|
||||
) -> Result<Value, RpcError> {
|
||||
match cli.command {
|
||||
cli::CliCommands::Receive { timeout } => {
|
||||
let mut stream = client
|
||||
.subscribe_receive(cli.account)
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {:?}", e))?;
|
||||
let mut stream = client.subscribe_receive(cli.account).await?;
|
||||
|
||||
{
|
||||
while let Some(v) = stream_next(timeout, &mut stream).await {
|
||||
let v = v.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {:?}", e))?;
|
||||
let v = v?;
|
||||
println!("{v}");
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
Ok(Value::Null)
|
||||
}
|
||||
cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
||||
cli::CliCommands::Block {
|
||||
|
@ -54,7 +67,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
let url = client
|
||||
.start_link(cli.account)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command startLink failed: {e:?}",))?
|
||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||
.device_link_uri;
|
||||
println!("{}", url);
|
||||
client.finish_link(url, name).await
|
||||
|
@ -349,18 +362,28 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
pin,
|
||||
} => client.verify(cli.account, verification_code, pin).await,
|
||||
cli::CliCommands::Version => client.version().await,
|
||||
};
|
||||
|
||||
result
|
||||
.map(|v| println!("{v}"))
|
||||
.map_err(|e| anyhow::anyhow!("JSON-RPC command failed: {e:?}",))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(cli: &cli::Cli) -> Result<jsonrpc::SignalCliClient, RpcError> {
|
||||
if let Some(tcp) = cli.json_rpc_tcp {
|
||||
async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
||||
if let Some(http) = &cli.json_rpc_http {
|
||||
let uri = if let Some(uri) = http {
|
||||
uri
|
||||
} else {
|
||||
DEFAULT_HTTP
|
||||
};
|
||||
let client = jsonrpc::connect_http(uri)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
} else if let Some(tcp) = cli.json_rpc_tcp {
|
||||
let socket_addr = tcp.unwrap_or_else(|| DEFAULT_TCP.parse().unwrap());
|
||||
jsonrpc::connect_tcp(socket_addr).await
|
||||
let client = jsonrpc::connect_tcp(socket_addr)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
} else {
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
|
@ -374,13 +397,17 @@ async fn connect(cli: &cli::Cli) -> Result<jsonrpc::SignalCliClient, RpcError> {
|
|||
})
|
||||
})
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
jsonrpc::connect_unix(socket_path).await
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_next(
|
||||
timeout: f64,
|
||||
stream: &mut TypedSubscriptionStream<Value>,
|
||||
stream: &mut Subscription<Value>,
|
||||
) -> Option<Result<Value, RpcError>> {
|
||||
if timeout < 0.0 {
|
||||
stream.next().await
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
use jsonrpc_client_transports::{transports::duplex, RpcChannel, RpcError};
|
||||
use jsonrpc_core::futures_util::{SinkExt, StreamExt, TryStreamExt};
|
||||
use jsonrpc_server_utils::{codecs::StreamCodec, tokio_util::codec::Decoder};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
|
||||
/// Connect to a JSON-RPC TCP server.
|
||||
pub async fn connect<S: ToSocketAddrs, Client: From<RpcChannel>>(
|
||||
socket: S,
|
||||
) -> Result<Client, RpcError> {
|
||||
let connection = TcpStream::connect(socket)
|
||||
.await
|
||||
.map_err(|e| RpcError::Other(Box::new(e)))?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
let sink = sink.sink_map_err(|e| RpcError::Other(Box::new(e)));
|
||||
let stream = stream.map_err(|e| log::error!("TCP stream error: {}", e));
|
||||
|
||||
let (client, sender) = duplex(
|
||||
Box::pin(sink),
|
||||
Box::pin(
|
||||
stream
|
||||
.take_while(|x| std::future::ready(x.is_ok()))
|
||||
.map(|x| x.expect("Stream is closed upon first error.")),
|
||||
),
|
||||
);
|
||||
|
||||
tokio::spawn(client);
|
||||
|
||||
Ok(sender.into())
|
||||
}
|
23
client/src/transports/ipc.rs
Normal file
23
client/src/transports/ipc.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use std::path::Path;
|
||||
|
||||
use futures_util::stream::StreamExt;
|
||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||
use jsonrpsee::core::Error;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::stream_codec::StreamCodec;
|
||||
use super::{Receiver, Sender};
|
||||
|
||||
/// Connect to a JSON-RPC Unix Socket server.
|
||||
pub async fn connect(
|
||||
socket: impl AsRef<Path>,
|
||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
||||
let connection = UnixStream::connect(socket).await?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
|
||||
let sender = Sender { inner: sink };
|
||||
let receiver = Receiver { inner: stream };
|
||||
|
||||
Ok((sender, receiver))
|
||||
}
|
64
client/src/transports/mod.rs
Normal file
64
client/src/transports/mod.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||
use jsonrpsee::core::{
|
||||
async_trait,
|
||||
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod ipc;
|
||||
mod stream_codec;
|
||||
pub mod tcp;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum Errors {
|
||||
#[error("Other: {0}")]
|
||||
Other(String),
|
||||
#[error("Closed")]
|
||||
Closed,
|
||||
}
|
||||
|
||||
struct Sender<T: Send + Sink<String>> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||
for Sender<T>
|
||||
{
|
||||
type Error = Errors;
|
||||
|
||||
async fn send(&mut self, body: String) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.send(body)
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn close(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Receiver<T: Send + Stream> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||
for Receiver<T>
|
||||
{
|
||||
type Error = Errors;
|
||||
|
||||
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
|
||||
match self.inner.next().await {
|
||||
None => Err(Errors::Closed),
|
||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
|
||||
}
|
||||
}
|
||||
}
|
61
client/src/transports/stream_codec.rs
Normal file
61
client/src/transports/stream_codec.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use bytes::BytesMut;
|
||||
use std::{io, str};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
type Separator = u8;
|
||||
|
||||
/// Stream codec for streaming protocols (ipc, tcp)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StreamCodec {
|
||||
incoming_separator: Separator,
|
||||
outgoing_separator: Separator,
|
||||
}
|
||||
|
||||
impl StreamCodec {
|
||||
/// Default codec with streaming input data. Input can be both enveloped and not.
|
||||
pub fn stream_incoming() -> Self {
|
||||
StreamCodec::new(b'\n', b'\n')
|
||||
}
|
||||
|
||||
/// New custom stream codec
|
||||
pub fn new(incoming_separator: Separator, outgoing_separator: Separator) -> Self {
|
||||
StreamCodec {
|
||||
incoming_separator,
|
||||
outgoing_separator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for StreamCodec {
|
||||
type Item = String;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<Self::Item>> {
|
||||
if let Some(i) = buf
|
||||
.as_ref()
|
||||
.iter()
|
||||
.position(|&b| b == self.incoming_separator)
|
||||
{
|
||||
let line = buf.split_to(i);
|
||||
let _ = buf.split_to(1);
|
||||
|
||||
match str::from_utf8(line.as_ref()) {
|
||||
Ok(s) => Ok(Some(s.to_string())),
|
||||
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<String> for StreamCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, msg: String, buf: &mut BytesMut) -> io::Result<()> {
|
||||
let mut payload = msg.into_bytes();
|
||||
payload.push(self.outgoing_separator);
|
||||
buf.extend_from_slice(&payload);
|
||||
Ok(())
|
||||
}
|
||||
}
|
21
client/src/transports/tcp.rs
Normal file
21
client/src/transports/tcp.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use futures_util::stream::StreamExt;
|
||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||
use jsonrpsee::core::Error;
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::stream_codec::StreamCodec;
|
||||
use super::{Receiver, Sender};
|
||||
|
||||
/// Connect to a JSON-RPC TCP server.
|
||||
pub async fn connect(
|
||||
socket: impl ToSocketAddrs,
|
||||
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
|
||||
let connection = TcpStream::connect(socket).await?;
|
||||
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
|
||||
|
||||
let sender = Sender { inner: sink };
|
||||
let receiver = Receiver { inner: stream };
|
||||
|
||||
Ok((sender, receiver))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue