diff options
author | Daniel Schadt <kingdread@gmx.de> | 2021-03-06 02:09:40 +0100 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2021-03-06 02:09:40 +0100 |
commit | 7398f8b7e9e5df6fe791383406c834b44fddbe2b (patch) | |
tree | ebb2ec26f3de5e136b1c8bc9ad33f239e8a78074 /src | |
parent | 4b58c051ed700406f14728b54bb3e2a739536739 (diff) | |
download | ezau-7398f8b7e9e5df6fe791383406c834b44fddbe2b.tar.gz ezau-7398f8b7e9e5df6fe791383406c834b44fddbe2b.tar.bz2 ezau-7398f8b7e9e5df6fe791383406c834b44fddbe2b.zip |
initial support for matrix log posting
Similar to Discord posting, this now allows ezau to post a message to
the given Matrix room for every log.
The text handling is still pretty bad and should be reworked, but so
should the Discord one. This is just the initial support, now that the
actual posting works we can add some tests and proper text parsing,
together with unifying some of the logic between Discord and Matrix.
Note that this currently only works for unencrypted rooms!
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 => "❓", + } +} |