Files
dotfiles/rust/signal/src/lib.rs
T

79 lines
2.6 KiB
Rust

//! 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(());
}
}
}