chore: refactor code into modules
This commit is contained in:
parent
3c87d183af
commit
ba15c035b8
4 changed files with 215 additions and 201 deletions
102
src/bot.rs
Normal file
102
src/bot.rs
Normal 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
71
src/config.rs
Normal 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()
|
||||||
|
}
|
221
src/main.rs
221
src/main.rs
|
@ -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
22
src/wol.rs
Normal 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(())
|
||||||
|
}
|
Loading…
Reference in a new issue