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, } impl From 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> { 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!("{}
\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::>() .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::>(); format!("{}
\n{}", lines[0], lines[1..].join("
\n")) }) .collect::>() .join("
\n
\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 => "❓", } } #[cfg(test)] mod test { use super::*; use std::io::Cursor; use evtclib::Compression; static ARKK_LOG: &[u8] = include_bytes!("../test/logs/arkk.zevtc"); fn arkk_log() -> Log { evtclib::process_stream(Cursor::new(ARKK_LOG), Compression::Zip).unwrap() } #[test] fn test_insert_log_new_category() { let log = arkk_log(); let old_text = "\ Unknown x https://dps.report.example"; let new_text = insert_log(old_text, &log, "foobar"); assert_eq!("\ Unknown x https://dps.report.example 99 CM (Shattered Observatory) ✅ foobar", new_text); } #[test] fn test_insert_log_appending() { let log = arkk_log(); let old_text = "\ 99 CM (Shattered Observatory) x old log here"; let new_text = insert_log(old_text, &log, "foobar"); assert_eq!("\ 99 CM (Shattered Observatory) x old log here ✅ foobar", new_text); } #[test] fn test_insert_log_multiple() { let log = arkk_log(); let old_text = "\ 100 CM (Sunqua Peak) y some old log here 99 CM (Shattered Observatory) x old log here 98 CM (Nightmare) z raboof"; let new_text = insert_log(old_text, &log, "foobar"); assert_eq!("\ 100 CM (Sunqua Peak) y some old log here 99 CM (Shattered Observatory) x old log here ✅ foobar 98 CM (Nightmare) z raboof", new_text); } #[test] fn test_htmlify_single() { let plain = "\ Header Log 1 Log 2"; assert_eq!(htmlify(plain), "\ Header
Log 1
Log 2"); } #[test] fn test_htmlify_multiple() { let plain = "\ Header 1 Log 1 Log 2 Header 2 Log 3 Log 4"; assert_eq!(htmlify(plain), "\ Header 1
Log 1
Log 2

Header 2
Log 3
Log 4"); } }