use std::{ net::{Ipv4Addr, UdpSocket}, time::Duration, }; use anyhow::{Context, Result}; 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, SyncSettings, }; use serde::Deserialize; 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())) .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))) .await; let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); client.sync_forever(settings, callback).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")?; login_and_sync(config).await?; Ok(()) }