From c853a5078b0a8dee22bb69b971b8315f66033f49 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Mon, 5 Sep 2022 16:59:20 -0700 Subject: feat(tool/sendsms): a CLI to send SMS This is a new tool to send SMS via Twilio's API. For now it supports a single subcommand: reboot. Using that subcommand, a SMS will be send with the name of the host and the IP address for the defined network interface. This is useful to be notified when one of my machine reboot, and what's the IP for the main interface (this is useful since my ISP does not provide a static IP). Change-Id: I5886a2c77ebd344ab3befa51a6bdd3d65bcc85d4 --- tools/sendsms/src/config.rs | 23 ++++++++++++ tools/sendsms/src/main.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++ tools/sendsms/src/message.rs | 76 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 tools/sendsms/src/config.rs create mode 100755 tools/sendsms/src/main.rs create mode 100755 tools/sendsms/src/message.rs (limited to 'tools/sendsms/src') diff --git a/tools/sendsms/src/config.rs b/tools/sendsms/src/config.rs new file mode 100644 index 0000000..da9435a --- /dev/null +++ b/tools/sendsms/src/config.rs @@ -0,0 +1,23 @@ +use serde::Deserialize; +use std::path::PathBuf; + +#[derive(Deserialize, Debug)] +pub struct Config { + pub to: String, + pub from: String, + pub account_sid: String, + pub auth_token: String, + pub reboot: RebootConfig, +} + +#[derive(Deserialize, Debug)] +pub struct RebootConfig { + pub ifname: String, +} + +impl Config { + pub fn load_from_file(filename: &PathBuf) -> std::io::Result { + let content = std::fs::read_to_string(filename)?; + Ok(toml::from_str(&content)?) + } +} diff --git a/tools/sendsms/src/main.rs b/tools/sendsms/src/main.rs new file mode 100755 index 0000000..30e92ff --- /dev/null +++ b/tools/sendsms/src/main.rs @@ -0,0 +1,85 @@ +#![warn(rust_2018_idioms)] + +mod config; +mod message; + +use clap::{crate_version, Parser}; +use gethostname::gethostname; +use log::{error, info}; +use std::net::IpAddr; +use std::path::PathBuf; +use std::process::exit; + +#[derive(Parser, Debug)] +#[clap(name = "sendsms")] +#[clap(author = "Franck Cuny ")] +#[clap(version = crate_version!())] +#[clap(propagate_version = true)] +struct Args { + #[clap(short, long, value_parser)] + config: PathBuf, + + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Parser, Debug)] +enum SubCommand { + Reboot, +} + +fn main() { + env_logger::init(); + let args = Args::parse(); + + let config: config::Config = match config::Config::load_from_file(&args.config) { + Ok(r) => r, + Err(e) => { + error!( + "unable to load data from {}: {}", + args.config.display(), + e.to_string() + ); + exit(1); + } + }; + + let body = match args.subcmd { + SubCommand::Reboot => reboot(&config.reboot), + }; + + let msg = message::Message { + from: config.from.to_owned(), + to: config.to.to_owned(), + body, + }; + + match msg.send(&config) { + Ok(_) => info!("message sent successfully"), + Err(error) => { + error!("failed to send the message: {}", error); + exit(1); + } + } +} + +fn reboot(config: &config::RebootConfig) -> String { + let ipaddr_v4 = if_addrs::get_if_addrs() + .unwrap_or_default() + .into_iter() + .find(|iface| iface.name == config.ifname) + .and_then(|iface| match iface.ip() { + IpAddr::V4(addr) => Some(addr), + IpAddr::V6(_) => None, + }) + .expect("there should be an ipv4 address"); + + let hostname = gethostname() + .into_string() + .expect("failed to get the hostname"); + + format!( + "{} has rebooted. The IP address for the interface {} is {}.", + hostname, config.ifname, ipaddr_v4 + ) +} diff --git a/tools/sendsms/src/message.rs b/tools/sendsms/src/message.rs new file mode 100755 index 0000000..9aa94a4 --- /dev/null +++ b/tools/sendsms/src/message.rs @@ -0,0 +1,76 @@ +use crate::config::Config; +use reqwest::blocking::Client; +use serde::Deserialize; +use std::collections::HashMap; +use std::fmt::{self, Display, Formatter}; + +const TWILIO_BASE_URL: &str = "https://api.twilio.com/2010-04-01/Accounts"; + +#[derive(Deserialize, Debug)] +pub struct Message { + pub from: String, + pub to: String, + pub body: String, +} + +// list of possible values: https://www.twilio.com/docs/sms/api/message-resource#message-status-values +#[derive(Debug, Deserialize, Clone)] +#[allow(non_camel_case_types)] +pub enum MessageStatus { + accepted, + scheduled, + queued, + sending, + sent, + receiving, + received, + delivered, + undelivered, + failed, + read, + canceled, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct MessageResponse { + pub status: Option, +} + +#[derive(Debug)] +pub enum TwilioError { + HTTPError(reqwest::StatusCode), +} + +impl Display for TwilioError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + TwilioError::HTTPError(ref s) => write!(f, "Invalid HTTP status code: {}", s), + } + } +} + +impl Message { + pub fn send(&self, config: &Config) -> Result { + let url = format!("{}/{}/Messages.json", TWILIO_BASE_URL, config.account_sid); + + let mut form = HashMap::new(); + form.insert("From", &self.from); + form.insert("To", &self.to); + form.insert("Body", &self.body); + + let client = Client::new(); + let response = client + .post(url) + .basic_auth(&config.account_sid, Some(&config.auth_token)) + .form(&form) + .send() + .unwrap(); + + match response.status() { + reqwest::StatusCode::CREATED | reqwest::StatusCode::OK => {} + other => return Err(TwilioError::HTTPError(other)), + }; + + Ok(response.json().unwrap()) + } +} -- cgit v1.2.3