diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 17 | ||||
-rw-r--r-- | src/discord.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 19 | ||||
-rw-r--r-- | src/matrix.rs | 187 |
4 files changed, 223 insertions, 4 deletions
diff --git a/src/config.rs b/src/config.rs index 47d6de7..ade4d2b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,8 @@ pub struct Config { pub zip: bool, /// Option Discord information for bot postings. pub discord: Option<Discord>, + /// Optional Matrix information for bot postings. + pub matrix: Option<Matrix>, } /// Configuration pertaining to the Discord posting. @@ -33,6 +35,21 @@ pub struct Discord { pub channel_id: u64, } +/// Configuration pertaining to the Matrix posting. +#[derive(Debug, Clone, Deserialize)] +pub struct Matrix { + /// Matrix homeserver. + pub homeserver: String, + /// Matrix username. + pub username: String, + /// Matrix password. + pub password: String, + /// Device ID, or None if a new one should be generated. + pub device_id: Option<String>, + /// Room ID where the message should be posted to. + pub room_id: String, +} + /// Attempt to load the configuration from the given file. pub fn load<P: AsRef<Path>>(path: P) -> Result<Config> { let content = fs::read(path)?; diff --git a/src/discord.rs b/src/discord.rs index 976b2d6..4d59580 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -87,7 +87,9 @@ impl EventHandler for Handler { } } -pub fn post_link(discord_token: &str, channel_id: u64, log: Log, link: String) -> Result<()> { +pub fn post_link(discord_token: &str, channel_id: u64, log: &Log, link: &str) -> Result<()> { + let link = link.to_owned(); + let log = log.clone(); let mut rt = Runtime::new()?; rt.block_on(async { diff --git a/src/main.rs b/src/main.rs index e67bd1b..43c98b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ use categories::Categorizable; mod config; use config::Config; mod discord; +mod matrix; const DPS_REPORT_API: &str = "https://dps.report/uploadContent"; const WATCH_DELAY_SECONDS: u64 = 2; @@ -78,11 +79,17 @@ fn inner_main(opts: &Opts) -> Result<()> { SubCommand::Upload(u) => { let permalink = upload_log(&u.path)?; println!("{}", permalink); + + let log = load_log(&u.path)?; if let Some(d) = &config.discord { - let log = load_log(&u.path)?; - discord::post_link(&d.auth_token, d.channel_id, log, permalink) + discord::post_link(&d.auth_token, d.channel_id, &log, &permalink) .context("Could not post link to Discord")?; } + + if let Some(m) = &config.matrix { + matrix::post_link(m.clone().into(), &m.room_id, &log, &permalink) + .context("Could not post link to Matrix")?; + } } } Ok(()) @@ -184,11 +191,17 @@ fn handle_file(config: &Config, filename: &Path) -> Result<()> { info!("Uploaded log, available at {}", permalink); if let Some(d) = &config.discord { - discord::post_link(&d.auth_token, d.channel_id, log, permalink) + discord::post_link(&d.auth_token, d.channel_id, &log, &permalink) .context("Could not post link to Discord")?; info!("Posted link to Discord"); } + if let Some(m) = &config.matrix { + matrix::post_link(m.clone().into(), &m.room_id, &log, &permalink) + .context("Could not post link to Matrix")?; + info!("Posted link to Matrix"); + } + Ok(()) } diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..5e9b2b7 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,187 @@ +use super::categories::Categorizable; +use super::config; + +use std::convert::TryFrom; +use std::time::{Duration, SystemTime}; + +use anyhow::Result; +use evtclib::{Log, Outcome}; +use log::{debug, info}; +use tokio::runtime::Runtime; + +use matrix_sdk::{ + api::r0::message::get_message_events, + events::room::message::{MessageEventContent, Relation, TextMessageEventContent}, + events::room::relationships::Replacement, + events::{AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent}, + identifiers::{EventId, RoomId, UserId}, + Client, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MatrixUser { + pub homeserver: String, + pub username: String, + pub password: String, + pub device_id: Option<String>, +} + +impl From<config::Matrix> for MatrixUser { + fn from(matrix: config::Matrix) -> Self { + MatrixUser { + homeserver: matrix.homeserver, + username: matrix.username, + password: matrix.password, + device_id: matrix.device_id, + } + } +} + +pub fn post_link(user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Result<()> { + let mut rt = Runtime::new()?; + let room_id = RoomId::try_from(room_id)?; + + rt.block_on(async { + let client = Client::new(&user.homeserver as &str)?; + let my_data = client + .login( + &user.username, + &user.password, + user.device_id.as_ref().map(|x| x as &str), + None, + ) + .await?; + info!("Matrix connected as {:?}", my_data.user_id); + + let old_msg = find_message(&client, &my_data.user_id, &room_id).await?; + + match old_msg { + None => { + debug!("Creating a fresh message for matrix"); + post_new(&client, &room_id, log, link).await?; + } + Some((old_id, old_text)) => { + debug!("Updating message {:?}", old_id); + let new_text = insert_log(&old_text, log, link); + let new_html = htmlify(&new_text); + update_message(&client, &room_id, &old_id, &new_text, &new_html).await?; + } + } + Ok(()) + }) +} + +/// Finds the right message if there is one to edit. +/// +/// Either returns the message ID and the old message text, or None if no suitable message was +/// found. +async fn find_message( + client: &Client, + my_id: &UserId, + room_id: &RoomId, +) -> Result<Option<(EventId, String)>> { + let request = get_message_events::Request::backward(room_id, ""); + let five_h_ago = SystemTime::now() - Duration::from_secs(5 * 60 * 60); + for raw_message in client.room_messages(request).await?.chunk { + if let Ok(message) = raw_message.deserialize() { + if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(msg)) = message { + if &msg.sender == my_id && msg.origin_server_ts >= five_h_ago { + if let MessageEventContent::Text(text) = msg.content { + if text.relates_to.is_none() { + return Ok(Some((msg.event_id, text.body))); + } + } + } + } + } + } + Ok(None) +} + +async fn post_new(client: &Client, room_id: &RoomId, log: &Log, link: &str) -> Result<()> { + let title = log.category(); + let line = format!("{} {}", state_emoji(log), link); + let body = format!("{}\n{}\n", title, line); + let html = format!("<b>{}</b><br>\n{}\n", title, line); + + let text_message = TextMessageEventContent::html(body, html); + client + .room_send( + room_id, + AnyMessageEventContent::RoomMessage(MessageEventContent::Text(text_message)), + None, + ) + .await?; + Ok(()) +} + +async fn update_message( + client: &Client, + room_id: &RoomId, + old_id: &EventId, + new_text: &str, + new_html: &str, +) -> Result<()> { + let mut message = TextMessageEventContent::html(new_text, new_html); + message.new_content = Some(Box::new(MessageEventContent::Text( + TextMessageEventContent::html(new_text, new_html), + ))); + message.relates_to = Some(Relation::Replacement(Replacement { + event_id: old_id.clone(), + })); + client + .room_send( + room_id, + AnyMessageEventContent::RoomMessage(MessageEventContent::Text(message)), + None, + ) + .await?; + Ok(()) +} + +fn insert_log(old_text: &str, log: &Log, link: &str) -> String { + let chunks = old_text.split("\n\n"); + let mut found = false; + let result = chunks + .map(|chunk| { + let category = chunk.split("\n").next().unwrap(); + if category == log.category() { + found = true; + format!("{}\n{} {}", chunk.trim(), state_emoji(log), link) + } else { + chunk.to_string() + } + }) + .collect::<Vec<_>>() + .join("\n\n"); + if found { + result + } else { + format!( + "{}\n\n{}\n{} {}", + result.trim(), + log.category(), + state_emoji(log), + link + ) + } +} + +fn htmlify(text: &str) -> String { + text.split("\n\n") + .map(|chunk| { + let lines = chunk.split("\n").collect::<Vec<_>>(); + format!("<b>{}</b><br>\n{}", lines[0], lines[1..].join("<br>\n")) + }) + .collect::<Vec<_>>() + .join("<br>\n<br>\n") +} + +fn state_emoji(log: &Log) -> &'static str { + let outcome = log.analyzer().and_then(|a| a.outcome()); + match outcome { + Some(Outcome::Success) => "✅", + Some(Outcome::Failure) => "❌", + None => "❓", + } +} |