diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..46d7867 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,102 @@ +use crate::{config::Host, wol}; + +use std::time::Duration; + +use tracing::{error, info}; + +use async_trait::async_trait; +use matrix_sdk::{ + self, + events::{ + room::{ + member::MemberEventContent, + message::{MessageEvent, MessageEventContent, TextMessageEventContent}, + }, + stripped::StrippedStateEvent, + }, + Client, EventEmitter, SyncRoom, +}; + +pub(crate) struct WakeOnLanBot { + pub(crate) client: Client, + pub(crate) hosts: Vec, +} + +impl WakeOnLanBot { + pub fn new(client: Client, hosts: Vec) -> Self { + Self { client, hosts } + } +} + +#[async_trait] +impl EventEmitter for WakeOnLanBot { + async fn on_stripped_state_member( + &self, + room: SyncRoom, + _room_member: &StrippedStateEvent, + _: Option, + ) { + match room { + SyncRoom::Invited(room) => { + let room = room.read().await; + info!("Autojoining room {}", room.room_id); + let mut delay = 2; + + while let Err(err) = self.client.join_room_by_id(&room.room_id).await { + // retry autojoin due to synapse sending invites, before the + // invited user can join for more information see + // https://github.com/matrix-org/synapse/issues/4345 + info!( + "Failed to join room {} ({:?}), retrying in {}s", + room.room_id, err, delay + ); + + tokio::time::delay_for(Duration::from_secs(delay)).await; + delay *= 2; + + if delay > 3600 { + error!("Can't join room {} ({:?})", room.room_id, err); + break; + } + } + info!("Successfully joined room {}", room.room_id); + } + _ => {} + } + } + async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) { + match room { + SyncRoom::Joined(_room) => { + let msg_body = if let MessageEvent { + content: + MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), + .. + } = event + { + msg_body.clone() + } else { + String::new() + }; + + for host in &self.hosts { + //TODO: pull the host name out of the message and check it. If the hostname is + //invalid, complain + if msg_body == format!("!wake {}", host.name) { + if host.users.contains(&event.sender.to_string()) { + info!("Waking host {}", host.name); + //TODO: reply here + match wol::wake(host.mac_addr) { + Ok(()) => info!("Magic packet sent to {} successfully", host.name), + Err(e) => error!( + "Couldn't send magic packet to {}, error: {}", + host.name, e + ), + } + } + } + } + } + _ => {} + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4569a46 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,71 @@ +use anyhow::{Context, Result}; +use clap::{clap_app, crate_authors, crate_description, crate_name, crate_version}; +use serde::Deserialize; +use tracing::info; + +#[derive(Deserialize)] +pub(crate) struct Config { + pub(crate) hs_url: String, + pub(crate) username: String, + pub(crate) password: String, + pub(crate) hosts: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct Host { + pub(crate) name: String, + pub(crate) mac_addr: [u8; 6], + pub(crate) users: Vec, +} + +pub(crate) fn setup_logging(level: u64) { + let level = match level { + 0 => log::LevelFilter::Error, + 1 => log::LevelFilter::Warn, + 2 => log::LevelFilter::Info, + 3 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }; + match fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}][{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + record.target(), + message + )) + }) + .level(level) + //This line is to avoid being flooded with event loop messages + //(one per thread and second, so 12Hz for a hyperthreaded hexacore) + //while running with LOG_LEVEL=debug + .level_for("tokio_reactor", log::LevelFilter::Error) + .level_for("tokio_core", log::LevelFilter::Error) + .chain(std::io::stdout()) + .apply() + { + Err(_) => { + eprintln!("error setting up logging!"); + } + _ => info!("logging set up properly"), + } +} + +pub(crate) fn read_config(path: &str) -> Result { + let config_file_content = std::fs::read_to_string(path).context("Couldn't read config file")?; + Ok(toml::from_str(&config_file_content).context("Couldn't parse config file")?) +} + +pub(crate) fn setup_clap() -> clap::ArgMatches<'static> { + clap_app!(myapp => + (name: crate_name!()) + (version: crate_version!()) + (author: crate_authors!()) + (about: crate_description!()) + (@arg config: +required "Set config file") + (@arg v: -v --verbose ... "Be verbose (you can add this up to 4 times for more logs). + By default, only errors are logged, so no output is a good thing.") + ) + .get_matches() +} diff --git a/src/main.rs b/src/main.rs index 8add25e..8aeb9da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,230 +1,49 @@ -use std::{ - net::{Ipv4Addr, UdpSocket}, - time::Duration, -}; +mod bot; +mod config; +mod wol; + +use config::Config; + +use bot::WakeOnLanBot; use anyhow::{Context, Result}; -use tracing::{error, info}; +use tracing::info; -use async_trait::async_trait; -use matrix_sdk::{ - self, - events::{ - room::{ - member::MemberEventContent, - message::{MessageEvent, MessageEventContent, TextMessageEventContent}, - }, - stripped::StrippedStateEvent, - }, - Client, EventEmitter, SyncRoom, SyncSettings, -}; -use serde::Deserialize; +use matrix_sdk::{self, Client, SyncSettings}; use url::Url; -use clap::{clap_app, crate_authors, crate_description, crate_name, crate_version}; - -#[derive(Deserialize)] -struct Config { - hs_url: String, - username: String, - password: String, - hosts: Vec, -} - -struct CommandBot { - /// This clone of the `Client` will send requests to the server, - /// while the other keeps us in sync with the server using `sync`. - client: Client, - hosts: Vec, -} - -#[derive(Deserialize)] -struct Host { - name: String, - mac_addr: [u8; 6], - users: Vec, -} - -impl CommandBot { - pub fn new(client: Client, hosts: Vec) -> Self { - Self { client, hosts } - } -} - -#[async_trait] -impl EventEmitter for CommandBot { - async fn on_stripped_state_member( - &self, - room: SyncRoom, - _room_member: &StrippedStateEvent, - _: Option, - ) { - match room { - SyncRoom::Invited(room) => { - let room = room.read().await; - info!("Autojoining room {}", room.room_id); - let mut delay = 2; - - while let Err(err) = self.client.join_room_by_id(&room.room_id).await { - // retry autojoin due to synapse sending invites, before the - // invited user can join for more information see - // https://github.com/matrix-org/synapse/issues/4345 - info!( - "Failed to join room {} ({:?}), retrying in {}s", - room.room_id, err, delay - ); - - tokio::time::delay_for(Duration::from_secs(delay)).await; - delay *= 2; - - if delay > 3600 { - error!("Can't join room {} ({:?})", room.room_id, err); - break; - } - } - info!("Successfully joined room {}", room.room_id); - } - _ => {} - } - } - async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) { - match room { - SyncRoom::Joined(_room) => { - let msg_body = if let MessageEvent { - content: - MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), - .. - } = event - { - msg_body.clone() - } else { - String::new() - }; - - for host in &self.hosts { - //TODO: pull the host name out of the message and check it. If the hostname is - //invalid, complain - if msg_body == format!("!wake {}", host.name) { - if host.users.contains(&event.sender.to_string()) { - info!("Waking host {}", host.name); - //TODO: reply here - match wake(host.mac_addr) { - Ok(()) => info!("Magic packet sent to {} successfully", host.name), - Err(e) => error!( - "Couldn't send magic packet to {}, error: {}", - host.name, e - ), - } - } - } - } - } - _ => {} - } - } -} - -fn wake(host: [u8; 6]) -> Result<()> { - let mut magic_packet = vec![0xffu8; 6]; - let mut payload = host - .to_vec() - .iter() - .map(|x| *x) - .cycle() - .take(16 * 6) - .collect::>(); - magic_packet.append(&mut payload); - - let socket = UdpSocket::bind((Ipv4Addr::new(0, 0, 0, 0), 0)).context("")?; - socket.set_broadcast(true).context("Couldn't set socket to broadcasting")?; - socket.send_to(&magic_packet[..], (Ipv4Addr::new(255, 255, 255, 255), 9))?; - - Ok(()) -} - -async fn callback(_resp: matrix_sdk::api::r0::sync::sync_events::Response) { - () -} - async fn login_and_sync(config: Config) -> Result<()> { let homeserver_url = Url::parse(&config.hs_url).expect("Couldn't parse the homeserver URL"); let mut client = Client::new(homeserver_url).unwrap(); client - .login(&config.username, &config.password, None, Some(&"command bot".to_string())) + .login( + &config.username, + &config.password, + None, + Some(&"command bot".to_string()), + ) .await?; info!("logged in as {}", config.username); client.sync(SyncSettings::default()).await.unwrap(); client - .add_event_emitter(Box::new(CommandBot::new(client.clone(), config.hosts))) + .add_event_emitter(Box::new(WakeOnLanBot::new(client.clone(), config.hosts))) .await; let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); - client.sync_forever(settings, callback).await; + client.sync_forever(settings, |_| async {}).await; Ok(()) } -fn setup_logging(level: u64) { - let level = match level { - 0 => log::LevelFilter::Error, - 1 => log::LevelFilter::Warn, - 2 => log::LevelFilter::Info, - 3 => log::LevelFilter::Debug, - _ => log::LevelFilter::Trace, - }; - match fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "[{}][{}][{}] {}", - chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), - record.level(), - record.target(), - message - )) - }) - .level(level) - //This line is to avoid being flooded with event loop messages - //(one per thread and second, so 12Hz for a hyperthreaded hexacore) - //while running with LOG_LEVEL=debug - .level_for("tokio_reactor", log::LevelFilter::Error) - .level_for("tokio_core", log::LevelFilter::Error) - .chain(std::io::stdout()) - .apply() - { - Err(_) => { - eprintln!("error setting up logging!"); - } - _ => info!("logging set up properly"), - } -} - -fn read_config(path: &str) -> Result { - let config_file_content = std::fs::read_to_string(path).context("Couldn't read config file")?; - Ok(toml::from_str(&config_file_content).context("Couldn't parse config file")?) -} - -fn setup_clap() -> clap::ArgMatches<'static> { - clap_app!(myapp => - (name: crate_name!()) - (version: crate_version!()) - (author: crate_authors!()) - (about: crate_description!()) - (@arg config: +required "Set config file") - (@arg v: -v --verbose ... "Be verbose (you can add this up to 4 times for more logs). -By default, only errors are logged, so no output is a good thing.") - ) - .get_matches() -} - #[tokio::main] async fn main() -> Result<()> { - let matches = setup_clap(); - setup_logging(matches.occurrences_of("v")); - let config = read_config("config.toml").context("Couldn't load config")?; + let matches = config::setup_clap(); + config::setup_logging(matches.occurrences_of("v")); + let config = config::read_config("config.toml").context("Couldn't load config")?; login_and_sync(config).await?; Ok(()) } diff --git a/src/wol.rs b/src/wol.rs new file mode 100644 index 0000000..b8893e7 --- /dev/null +++ b/src/wol.rs @@ -0,0 +1,22 @@ +use anyhow::{Context, Result}; +use std::net::{Ipv4Addr, UdpSocket}; + +pub(crate) fn wake(host: [u8; 6]) -> Result<()> { + let mut magic_packet = vec![0xffu8; 6]; + let mut payload = host + .to_vec() + .iter() + .map(|x| *x) + .cycle() + .take(16 * 6) + .collect::>(); + magic_packet.append(&mut payload); + + let socket = UdpSocket::bind((Ipv4Addr::new(0, 0, 0, 0), 0)).context("")?; + socket + .set_broadcast(true) + .context("Couldn't set socket to broadcasting")?; + socket.send_to(&magic_packet[..], (Ipv4Addr::new(255, 255, 255, 255), 9))?; + + Ok(()) +}