use super::categories::Categorizable; use super::config; use super::logbag::{state_emoji, LogBag}; use std::convert::TryFrom; use std::time::{Duration, SystemTime}; use anyhow::Result; use evtclib::Log; 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, } } } /// Maximum age of the message to still be edited. const MAX_HOURS: u64 = 5; /// Posts a link to the log to a Matrix room. /// /// The user identification is given in the `user` parameter. /// /// This function blocks until all API calls have been made, that is until the message has reached /// the homeserver. 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 logbag = insert_log(&old_text, log, link); let new_text = logbag.render_plain(); let new_html = logbag.render_html(); 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 time_limit = SystemTime::now() - Duration::from_secs(MAX_HOURS * 60 * 60); for raw_message in client.room_messages(request).await?.chunk { if let Ok(AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(msg))) = raw_message.deserialize() { if &msg.sender == my_id && msg.origin_server_ts >= time_limit { if let MessageEventContent::Text(text) = msg.content { if text.relates_to.is_none() { return Ok(Some((msg.event_id, text.body))); } } } } } Ok(None) } /// Post a new message to the given Matrix channel. async fn post_new(client: &Client, room_id: &RoomId, log: &Log, link: &str) -> Result<()> { let line = format!("{} {}", state_emoji(log), link); let logbag: LogBag = vec![(log.category().to_string(), vec![line])].into(); let body = logbag.render_plain(); let html = logbag.render_html(); let text_message = TextMessageEventContent::html(body, html); client .room_send( room_id, AnyMessageEventContent::RoomMessage(MessageEventContent::Text(text_message)), None, ) .await?; Ok(()) } /// Updates the given message with some new text /// /// This constructs and sends the right Matrix API message. 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(()) } /// Inserts the given log into the text. /// /// The text is first parsed as a [`LogBag`], then the link is formatted and inserted. fn insert_log(old_text: &str, log: &Log, link: &str) -> LogBag { let line = format!("{} {}", state_emoji(log), link); let mut logbag = LogBag::parse_plain(old_text).unwrap(); logbag.insert(log.category(), line); logbag }