79 lines
2.6 KiB
Rust
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(());
|
|
}
|
|
}
|
|
}
|