about summary refs log tree commit diff
path: root/tools/sendsms/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/sendsms/src/config.rs23
-rwxr-xr-xtools/sendsms/src/main.rs85
-rwxr-xr-xtools/sendsms/src/message.rs76
3 files changed, 184 insertions, 0 deletions
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<Config> {
+        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 <franck@fcuny.net>")]
+#[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<MessageStatus>,
+}
+
+#[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<MessageResponse, TwilioError> {
+        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())
+    }
+}