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