//! Send alerts through the local signal-cli daemon on Unix systems. //! //! ```no_run //! use signal::Signal; //! //! let client = Signal { //! from_phone: "+15555550100".into(), //! }; //! client.send("+15555550101", "Service failed")?; //! client.send("+15555550102", "Service recovered")?; //! # Ok::<(), anyhow::Error>(()) //! ``` use std::{ io::{BufRead, BufReader, Write}, os::unix::net::UnixStream, time::Duration, }; use anyhow::{Context, Result, bail}; use serde_json::{Value, json}; /// A sending account for alerts through `/run/signal-cli/socket`. pub struct Signal { /// The phone number of an account registered with the local daemon. pub from_phone: String, } impl Signal { /// Send a message to `to_phone` and wait for the corresponding JSON-RPC response. /// /// Socket reads and writes use a four-second timeout. /// /// # Errors /// /// Returns an error if connecting, writing, or reading fails, the daemon /// returns invalid JSON or an RPC error, or it closes before responding. pub fn send(&self, to_phone: &str, body: &str) -> Result<()> { let mut stream = UnixStream::connect("/run/signal-cli/socket") .context("Failed to connect to Signal RPC socket")?; let timeout = Duration::from_secs(4); stream.set_read_timeout(Some(timeout))?; stream.set_write_timeout(Some(timeout))?; let request = json!({ "jsonrpc": "2.0", "method": "send", "params": { "account": self.from_phone, "recipient": [to_phone], "message": body, }, "id": "signal-alert", }); writeln!(stream, "{request}").context("Failed to send Signal RPC request")?; let mut reader = BufReader::new(stream); let mut line = String::new(); loop { line.clear(); if reader .read_line(&mut line) .context("Failed to read Signal RPC response")? == 0 || !line.ends_with('\n') { bail!("Signal RPC socket closed before returning a response"); } let response: Value = serde_json::from_str(&line).context("Invalid Signal RPC response")?; if response.get("id").and_then(Value::as_str) != Some("signal-alert") { continue; } if let Some(error) = response.get("error").filter(|value| !value.is_null()) { bail!("Signal RPC returned an error: {error}"); } return Ok(()); } } }