chore: refactor code into modules

This commit is contained in:
Jan Christian Grünhage 2020-10-15 22:05:21 +02:00
parent 3c87d183af
commit ba15c035b8
4 changed files with 215 additions and 201 deletions

102
src/bot.rs Normal file
View file

@ -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<Host>,
}
impl WakeOnLanBot {
pub fn new(client: Client, hosts: Vec<Host>) -> Self {
Self { client, hosts }
}
}
#[async_trait]
impl EventEmitter for WakeOnLanBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
_room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
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
),
}
}
}
}
}
_ => {}
}
}
}

71
src/config.rs Normal file
View file

@ -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<Host>,
}
#[derive(Deserialize)]
pub(crate) struct Host {
pub(crate) name: String,
pub(crate) mac_addr: [u8; 6],
pub(crate) users: Vec<String>,
}
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<Config> {
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()
}

View file

@ -1,230 +1,49 @@
use std::{ mod bot;
net::{Ipv4Addr, UdpSocket}, mod config;
time::Duration, mod wol;
};
use config::Config;
use bot::WakeOnLanBot;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tracing::{error, info}; use tracing::info;
use async_trait::async_trait; use matrix_sdk::{self, Client, SyncSettings};
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 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<Host>,
}
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<Host>,
}
#[derive(Deserialize)]
struct Host {
name: String,
mac_addr: [u8; 6],
users: Vec<String>,
}
impl CommandBot {
pub fn new(client: Client, hosts: Vec<Host>) -> Self {
Self { client, hosts }
}
}
#[async_trait]
impl EventEmitter for CommandBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
_room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
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::<Vec<_>>();
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<()> { async fn login_and_sync(config: Config) -> Result<()> {
let homeserver_url = Url::parse(&config.hs_url).expect("Couldn't parse the homeserver URL"); let homeserver_url = Url::parse(&config.hs_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new(homeserver_url).unwrap(); let mut client = Client::new(homeserver_url).unwrap();
client client
.login(&config.username, &config.password, None, Some(&"command bot".to_string())) .login(
&config.username,
&config.password,
None,
Some(&"command bot".to_string()),
)
.await?; .await?;
info!("logged in as {}", config.username); info!("logged in as {}", config.username);
client.sync(SyncSettings::default()).await.unwrap(); client.sync(SyncSettings::default()).await.unwrap();
client client
.add_event_emitter(Box::new(CommandBot::new(client.clone(), config.hosts))) .add_event_emitter(Box::new(WakeOnLanBot::new(client.clone(), config.hosts)))
.await; .await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap()); let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync_forever(settings, callback).await; client.sync_forever(settings, |_| async {}).await;
Ok(()) 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<Config> {
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] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let matches = setup_clap(); let matches = config::setup_clap();
setup_logging(matches.occurrences_of("v")); config::setup_logging(matches.occurrences_of("v"));
let config = read_config("config.toml").context("Couldn't load config")?; let config = config::read_config("config.toml").context("Couldn't load config")?;
login_and_sync(config).await?; login_and_sync(config).await?;
Ok(()) Ok(())
} }

22
src/wol.rs Normal file
View file

@ -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::<Vec<_>>();
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(())
}