use crate::{config::Host, wol}; use std::{collections::HashMap, str::FromStr, time::Duration}; use tracing::{error, info}; use async_trait::async_trait; use matrix_sdk::{ self, identifiers::RoomId, events::{ room::{ member::MemberEventContent, message::{MessageEvent, MessageEventContent, TextMessageEventContent, NoticeMessageEventContent}, }, stripped::StrippedStateEvent, }, Client, EventEmitter, SyncRoom, }; use lazy_static::lazy_static; use regex::Regex; pub(crate) struct WakeOnLanBot { pub(crate) client: Client, pub(crate) hosts: HashMap, } impl WakeOnLanBot { pub fn new(client: Client, hosts: HashMap) -> 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() }; if let Ok(command) = Command::from_str(&msg_body) { handle_command(command, &self.client, &self.hosts, event, &room.read().await.room_id.clone()).await; } else { //TODO: give help text } } _ => {} } } } async fn handle_command(command: Command, client: &Client, hosts: &HashMap, event: &MessageEvent, room: &RoomId) { match command { Command::Wake { host } => { if let Some(host_conf) = hosts.get(&host) { if host_conf.users.contains(&event.sender.to_string()) { info!("Waking host {}", host); match wol::wake(host_conf.mac_addr) { Ok(()) => { info!("Magic packet sent to {} successfully", host); send_message(client, &format!("Successfully send magic packet to {}", host), room).await; } Err(e) => { error!("Couldn't send magic packet to {}, error: {}", host, e); send_message(client, &format!("Failed to send magic packet to {}", host), room).await; } } } else { send_message(client, &format!("No permission to wake up {}!", host), room).await; } } else { send_message(client, &format!("Host {} not found!", host), room).await; } } } } async fn send_message(client: &Client, message: &str, room: &RoomId) { client.room_send(room, MessageEventContent::Notice(NoticeMessageEventContent { body: message.to_owned(), format: None, formatted_body: None, relates_to: None, }), None).await.unwrap(); //TODO error handling here } enum Command { Wake { host: String }, } impl FromStr for Command { type Err = &'static str; fn from_str(s: &str) -> Result { lazy_static! { static ref RE_WAKE: Regex = Regex::new("!wake (?P.*)").unwrap(); } if let Some(captures) = RE_WAKE.captures(s) { return Ok(Self::Wake { host: captures["host"].to_string(), }); } else { return Err("Invalid command"); } } }