diff --git a/src/bot.rs b/src/bot.rs index 0333ff9..05f5766 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,4 +1,5 @@ -use crate::matrix::{Event, MessageContent, RoomEvent, SyncCommand}; +use crate::matrix::{Event, MessageContent, RoomEvent, SyncCommand, NoticeMessage}; +use crate::commands::parse_command; use reqwest::{Client, Url}; use serde::{self, Deserialize, Serialize}; use std::fs; @@ -15,6 +16,8 @@ pub struct MatrixConfig { /// The next batch to grab. This should be set automatically pub next_batch: Option, + #[serde(default)] + pub txn_id: u64, pub login: toml::Value, } @@ -34,6 +37,7 @@ pub struct DiceBot { next_batch: Option, client: Client, home_server: Url, + txn_id: u64, } #[derive(Deserialize, Debug)] @@ -60,6 +64,7 @@ impl DiceBot { .await?; let body: LoginResponse = serde_json::from_str(&response.text().await?)?; let next_batch = config.matrix.next_batch.clone(); + let txn_id = config.matrix.txn_id; Ok(DiceBot { home_server, config_path, @@ -67,6 +72,7 @@ impl DiceBot { config, access_token: body.access_token, next_batch, + txn_id, }) } @@ -84,9 +90,9 @@ impl DiceBot { /// Build a url using the current home server and the given path, as well as appending the /// access token - fn url(&self, path: &str, query: &[(&str, &str)]) -> Url { + fn url>(&self, path: S, query: &[(&str, &str)]) -> Url { let mut url = self.home_server.clone(); - url.set_path(path); + url.set_path(path.as_ref()); { let mut query_pairs = url.query_pairs_mut(); query_pairs.append_pair("access_token", &self.access_token); @@ -118,7 +124,7 @@ impl DiceBot { let sync: SyncCommand = serde_json::from_str(&body).unwrap(); // First join invited rooms for room in sync.rooms.invite.keys() { - let join_url = self.url(&format!("/_matrix/client/r0/rooms/{}/join", room), &[]); + let join_url = self.url(format!("/_matrix/client/r0/rooms/{}/join", room), &[]); self.client .post(join_url) .header("user-agent", USER_AGENT) @@ -126,16 +132,45 @@ impl DiceBot { .await?; } - for (_room_id, room) in sync.rooms.join.iter() { + for (room_id, room) in sync.rooms.join.iter() { for event in &room.timeline.events { if let Event::Room(RoomEvent { + sender, + event_id, content: MessageContent::Text(message), .. }) = event { - // TODO: create command parser (maybe command.rs) to parse !roll/!r commands - // and reply - println!("Body: {}", message.body()); + let (plain, html): (String, String) = match parse_command(message.body()) { + Ok(Some(command)) => { + let command = command.execute(); + (command.plain().into(), command.html().into()) + }, + Ok(None) => continue, + Err(e) => { + let message = format!("Error parsing command: {}", e); + let html_message = format!("

{}

", message); + (message, html_message) + }, + }; + + let plain = format!("{}\n{}", sender, plain); + let html = format!("

{}

\n{}", sender, plain); + + let message = NoticeMessage { + body: plain, + format: Some("org.matrix.custom.html".into()), + formatted_body: Some(html), + }; + + self.txn_id += 1; + let mut send_url = self.url(format!("/_matrix/client/r0/rooms/{}/send/m.room.message/{}", room_id, self.txn_id), &[]); + self.client + .put(send_url) + .header("user-agent", USER_AGENT) + .body(serde_json::to_string(&message)?) + .send() + .await?; } } } @@ -155,6 +190,7 @@ impl DiceBot { .await?; self.config.matrix.next_batch = self.next_batch; + self.config.matrix.txn_id = self.txn_id; if let Some(config_path) = self.config_path { let config = toml::to_string_pretty(&self.config)?; diff --git a/src/commands.rs b/src/commands.rs index e83cf0d..557dff5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -29,13 +29,16 @@ impl Command for RollCommand { let roll = self.0.roll(); let plain = format!("Dice: {}\nResult: {}", self.0, roll); let html = format!( - "Dice: {}
Result: {}", + "

Dice: {}

Result: {}

", self.0, roll ); Execution { plain, html } } } +/// Parse a command string into a dynamic command execution trait object. +/// Returns an error if a command was recognized but not parsed correctly. Returns None if no +/// command was recognized. pub fn parse_command(s: &str) -> Result>, String> { // Ignore trailing input, if any. match parser::parse_command(s) { diff --git a/src/matrix.rs b/src/matrix.rs index 637003f..9aaa410 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -1,11 +1,30 @@ use serde::{self, Deserialize, Serialize}; use std::collections::HashMap; +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "msgtype")] +#[serde(rename = "m.notice")] +pub struct NoticeMessage { + pub body: String, + + #[serde(default)] + pub format: Option, + + #[serde(default)] + pub formatted_body: Option, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "msgtype")] #[serde(rename = "m.text")] pub struct TextMessage { body: String, + + #[serde(default)] + format: Option, + + #[serde(default)] + formatted_body: Option, } impl TextMessage { @@ -26,6 +45,7 @@ pub enum MessageContent { pub struct RoomEvent { pub content: MessageContent, pub event_id: String, + pub sender: String, } #[derive(Serialize, Deserialize, Debug)]