diff options
Diffstat (limited to 'src/matrix.rs')
-rw-r--r-- | src/matrix.rs | 187 |
1 files changed, 187 insertions, 0 deletions
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 => "❓", + } +} |