]> nmode's Git Repositories - signal-cli/blob - client/src/main.rs
Unsubscribe in client after timeout
[signal-cli] / client / src / main.rs
1 use std::{path::PathBuf, time::Duration};
2
3 use clap::Parser;
4 use cli::Cli;
5 use jsonrpsee::core::client::{Subscription, SubscriptionClientT};
6 use jsonrpsee::core::Error as RpcError;
7 use serde_json::Value;
8 use tokio::{select, time::sleep};
9
10 use crate::cli::{GroupPermission, LinkState};
11 use crate::jsonrpc::RpcClient;
12
13 mod cli;
14 #[allow(non_snake_case, clippy::too_many_arguments)]
15 mod jsonrpc;
16 mod transports;
17
18 const DEFAULT_TCP: &str = "127.0.0.1:7583";
19 const DEFAULT_SOCKET_SUFFIX: &str = "signal-cli/socket";
20 const DEFAULT_HTTP: &str = "http://localhost:8080/api/v1/rpc";
21
22 #[tokio::main]
23 async fn main() -> Result<(), anyhow::Error> {
24 let cli = cli::Cli::parse();
25
26 let result = connect(cli).await;
27
28 match result {
29 Ok(Value::Null) => {}
30 Ok(v) => println!("{v}"),
31 Err(e) => return Err(anyhow::anyhow!("JSON-RPC command failed: {e:?}")),
32 }
33 Ok(())
34 }
35
36 async fn handle_command(
37 cli: Cli,
38 client: impl SubscriptionClientT + Sync,
39 ) -> Result<Value, RpcError> {
40 match cli.command {
41 cli::CliCommands::Receive { timeout } => {
42 let mut stream = client.subscribe_receive(cli.account).await?;
43
44 {
45 while let Some(v) = stream_next(timeout, &mut stream).await {
46 let v = v?;
47 println!("{v}");
48 }
49 }
50 stream.unsubscribe().await?;
51 Ok(Value::Null)
52 }
53 cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
54 cli::CliCommands::Block {
55 recipient,
56 group_id,
57 } => client.block(cli.account, recipient, group_id).await,
58 cli::CliCommands::DeleteLocalAccountData { ignore_registered } => {
59 client
60 .delete_local_account_data(cli.account, ignore_registered)
61 .await
62 }
63 cli::CliCommands::GetUserStatus { recipient } => {
64 client.get_user_status(cli.account, recipient).await
65 }
66 cli::CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
67 cli::CliCommands::Link { name } => {
68 let url = client
69 .start_link(cli.account)
70 .await
71 .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
72 .device_link_uri;
73 println!("{}", url);
74 client.finish_link(url, name).await
75 }
76 cli::CliCommands::ListAccounts => client.list_accounts().await,
77 cli::CliCommands::ListContacts {
78 recipient,
79 all_recipients,
80 blocked,
81 name,
82 } => {
83 client
84 .list_contacts(cli.account, recipient, all_recipients, blocked, name)
85 .await
86 }
87 cli::CliCommands::ListDevices => client.list_devices(cli.account).await,
88 cli::CliCommands::ListGroups {
89 detailed: _,
90 group_id,
91 } => client.list_groups(cli.account, group_id).await,
92 cli::CliCommands::ListIdentities { number } => {
93 client.list_identities(cli.account, number).await
94 }
95 cli::CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
96 cli::CliCommands::QuitGroup {
97 group_id,
98 delete,
99 admin,
100 } => {
101 client
102 .quit_group(cli.account, group_id, delete, admin)
103 .await
104 }
105 cli::CliCommands::Register { voice, captcha } => {
106 client.register(cli.account, voice, captcha).await
107 }
108 cli::CliCommands::RemoveContact { recipient, forget } => {
109 client.remove_contact(cli.account, recipient, forget).await
110 }
111 cli::CliCommands::RemoveDevice { device_id } => {
112 client.remove_device(cli.account, device_id).await
113 }
114 cli::CliCommands::RemovePin => client.remove_pin(cli.account).await,
115 cli::CliCommands::RemoteDelete {
116 target_timestamp,
117 recipient,
118 group_id,
119 note_to_self,
120 } => {
121 client
122 .remote_delete(
123 cli.account,
124 target_timestamp,
125 recipient,
126 group_id,
127 note_to_self,
128 )
129 .await
130 }
131 cli::CliCommands::Send {
132 recipient,
133 group_id,
134 note_to_self,
135 end_session,
136 message,
137 attachment,
138 mention,
139 quote_timestamp,
140 quote_author,
141 quote_message,
142 quote_mention,
143 sticker,
144 story_timestamp,
145 story_author,
146 } => {
147 client
148 .send(
149 cli.account,
150 recipient,
151 group_id,
152 note_to_self,
153 end_session,
154 message.unwrap_or_default(),
155 attachment,
156 mention,
157 quote_timestamp,
158 quote_author,
159 quote_message,
160 quote_mention,
161 sticker,
162 story_timestamp,
163 story_author,
164 )
165 .await
166 }
167 cli::CliCommands::SendContacts => client.send_contacts(cli.account).await,
168 cli::CliCommands::SendPaymentNotification {
169 recipient,
170 receipt,
171 note,
172 } => {
173 client
174 .send_payment_notification(cli.account, recipient, receipt, note)
175 .await
176 }
177 cli::CliCommands::SendReaction {
178 recipient,
179 group_id,
180 note_to_self,
181 emoji,
182 target_author,
183 target_timestamp,
184 remove,
185 story,
186 } => {
187 client
188 .send_reaction(
189 cli.account,
190 recipient,
191 group_id,
192 note_to_self,
193 emoji,
194 target_author,
195 target_timestamp,
196 remove,
197 story,
198 )
199 .await
200 }
201 cli::CliCommands::SendReceipt {
202 recipient,
203 target_timestamp,
204 r#type,
205 } => {
206 client
207 .send_receipt(
208 cli.account,
209 recipient,
210 target_timestamp,
211 match r#type {
212 cli::ReceiptType::Read => "read".to_owned(),
213 cli::ReceiptType::Viewed => "viewed".to_owned(),
214 },
215 )
216 .await
217 }
218 cli::CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
219 cli::CliCommands::SendTyping {
220 recipient,
221 group_id,
222 stop,
223 } => {
224 client
225 .send_typing(cli.account, recipient, group_id, stop)
226 .await
227 }
228 cli::CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
229 cli::CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
230 client
231 .submit_rate_limit_challenge(cli.account, challenge, captcha)
232 .await
233 }
234 cli::CliCommands::Trust {
235 recipient,
236 trust_all_known_keys,
237 verified_safety_number,
238 } => {
239 client
240 .trust(
241 cli.account,
242 recipient,
243 trust_all_known_keys,
244 verified_safety_number,
245 )
246 .await
247 }
248 cli::CliCommands::Unblock {
249 recipient,
250 group_id,
251 } => client.unblock(cli.account, recipient, group_id).await,
252 cli::CliCommands::Unregister { delete_account } => {
253 client.unregister(cli.account, delete_account).await
254 }
255 cli::CliCommands::UpdateAccount { device_name } => {
256 client.update_account(cli.account, device_name).await
257 }
258 cli::CliCommands::UpdateConfiguration {
259 read_receipts,
260 unidentified_delivery_indicators,
261 typing_indicators,
262 link_previews,
263 } => {
264 client
265 .update_configuration(
266 cli.account,
267 read_receipts,
268 unidentified_delivery_indicators,
269 typing_indicators,
270 link_previews,
271 )
272 .await
273 }
274 cli::CliCommands::UpdateContact {
275 recipient,
276 expiration,
277 name,
278 } => {
279 client
280 .update_contact(cli.account, recipient, name, expiration)
281 .await
282 }
283 cli::CliCommands::UpdateGroup {
284 group_id,
285 name,
286 description,
287 avatar,
288 member,
289 remove_member,
290 admin,
291 remove_admin,
292 ban,
293 unban,
294 reset_link,
295 link,
296 set_permission_add_member,
297 set_permission_edit_details,
298 set_permission_send_messages,
299 expiration,
300 } => {
301 client
302 .update_group(
303 cli.account,
304 group_id,
305 name,
306 description,
307 avatar,
308 member,
309 remove_member,
310 admin,
311 remove_admin,
312 ban,
313 unban,
314 reset_link,
315 link.map(|link| match link {
316 LinkState::Enabled => "enabled".to_owned(),
317 LinkState::EnabledWithApproval => "enabledWithApproval".to_owned(),
318 LinkState::Disabled => "disabled".to_owned(),
319 }),
320 set_permission_add_member.map(|p| match p {
321 GroupPermission::EveryMember => "everyMember".to_owned(),
322 GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
323 }),
324 set_permission_edit_details.map(|p| match p {
325 GroupPermission::EveryMember => "everyMember".to_owned(),
326 GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
327 }),
328 set_permission_send_messages.map(|p| match p {
329 GroupPermission::EveryMember => "everyMember".to_owned(),
330 GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
331 }),
332 expiration,
333 )
334 .await
335 }
336 cli::CliCommands::UpdateProfile {
337 given_name,
338 family_name,
339 about,
340 about_emoji,
341 mobile_coin_address,
342 avatar,
343 remove_avatar,
344 } => {
345 client
346 .update_profile(
347 cli.account,
348 given_name,
349 family_name,
350 about,
351 about_emoji,
352 mobile_coin_address,
353 avatar,
354 remove_avatar,
355 )
356 .await
357 }
358 cli::CliCommands::UploadStickerPack { path } => {
359 client.upload_sticker_pack(cli.account, path).await
360 }
361 cli::CliCommands::Verify {
362 verification_code,
363 pin,
364 } => client.verify(cli.account, verification_code, pin).await,
365 cli::CliCommands::Version => client.version().await,
366 }
367 }
368
369 async fn connect(cli: Cli) -> Result<Value, RpcError> {
370 if let Some(http) = &cli.json_rpc_http {
371 let uri = if let Some(uri) = http {
372 uri
373 } else {
374 DEFAULT_HTTP
375 };
376 let client = jsonrpc::connect_http(uri)
377 .await
378 .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
379
380 handle_command(cli, client).await
381 } else if let Some(tcp) = cli.json_rpc_tcp {
382 let socket_addr = tcp.unwrap_or_else(|| DEFAULT_TCP.parse().unwrap());
383 let client = jsonrpc::connect_tcp(socket_addr)
384 .await
385 .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
386
387 handle_command(cli, client).await
388 } else {
389 let socket_path = cli
390 .json_rpc_socket
391 .clone()
392 .unwrap_or(None)
393 .or_else(|| {
394 std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
395 PathBuf::from(runtime_dir)
396 .join(DEFAULT_SOCKET_SUFFIX)
397 .into()
398 })
399 })
400 .unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
401 let client = jsonrpc::connect_unix(socket_path)
402 .await
403 .map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
404
405 handle_command(cli, client).await
406 }
407 }
408
409 async fn stream_next(
410 timeout: f64,
411 stream: &mut Subscription<Value>,
412 ) -> Option<Result<Value, RpcError>> {
413 if timeout < 0.0 {
414 stream.next().await
415 } else {
416 select! {
417 v = stream.next() => v,
418 _= sleep(Duration::from_millis((timeout * 1000.0) as u64)) => None,
419 }
420 }
421 }