aboutsummaryrefslogtreecommitdiff
path: root/src/matrix.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/matrix.rs')
-rw-r--r--src/matrix.rs187
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 => "❓",
+ }
+}