aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2021-03-06 12:12:03 +0100
committerDaniel Schadt <kingdread@gmx.de>2021-03-06 12:12:03 +0100
commitd84b4c3466adcc085b7df36015df4b46488ba591 (patch)
tree4193ff1cbc4fdeb57fdddff2d2b33339c3ef0efd /src
parent1eb73eb5211fb43e2389857dda4f7afa15e55433 (diff)
downloadezau-d84b4c3466adcc085b7df36015df4b46488ba591.tar.gz
ezau-d84b4c3466adcc085b7df36015df4b46488ba591.tar.bz2
ezau-d84b4c3466adcc085b7df36015df4b46488ba591.zip
add proper previous-log parsing for matrix
Doing all the "new log" insertion based on simple string operations is a bit of madness, so the proper course of action is to parse them into a proper intermediate representation from which we can then generate a plain and HTML body. In addition, this has some other minor code cleanup for the matrix module.
Diffstat (limited to 'src')
-rw-r--r--src/logbag.rs288
-rw-r--r--src/main.rs1
-rw-r--r--src/matrix.rs196
3 files changed, 326 insertions, 159 deletions
diff --git a/src/logbag.rs b/src/logbag.rs
new file mode 100644
index 0000000..f461007
--- /dev/null
+++ b/src/logbag.rs
@@ -0,0 +1,288 @@
+use std::iter;
+use std::str::FromStr;
+
+use evtclib::{Log, Outcome};
+use itertools::Itertools;
+
+/// A [`LogBag`] is a struct that holds multiple logs in their categories.
+///
+/// This is similar to hash map mapping a category to a list of logs, but the [`LogBag`] saves its
+/// insertion order. The logs are saved as a line, that way we don't have to re-parse or re-upload
+/// them and we can just handle arbitrary data.
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
+pub struct LogBag {
+ data: Vec<(String, Vec<String>)>,
+}
+
+impl LogBag {
+ /// Construct a new, empty [`LogBag`].
+ pub fn new() -> Self {
+ LogBag { data: Vec::new() }
+ }
+
+ /// Return an iterator over all available categories.
+ pub fn categories(&self) -> impl Iterator<Item = &str> {
+ self.data.iter().map(|x| &x.0 as &str)
+ }
+
+ /// Return an iterator over (category, items).
+ pub fn iter(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
+ self.data
+ .iter()
+ .map(|(cat, lines)| (cat as &str, lines.iter().map(|l| l as &str)))
+ }
+
+ /// Insert an item into the given category.
+ ///
+ /// If the category does not exist yet, it will be appended at the bottom.
+ pub fn insert(&mut self, category: &str, line: String) {
+ for (cat, lines) in self.data.iter_mut() {
+ if cat == category {
+ lines.push(line);
+ return;
+ }
+ }
+
+ // When we reach here, we don't have the category yet, so we gotta insert it.
+ self.data.push((String::from(category), vec![line]));
+ }
+
+ /// Tries to parse the given text as a plain [`LogBag`].
+ pub fn parse_plain(input: &str) -> Option<LogBag> {
+ input.parse().ok()
+ }
+
+ /// Renders the contents of this [`LogBag`] as plain text.
+ ///
+ /// The output of this can be fed back into [`LogBag::parse_plain`] to round-trip.
+ pub fn render_plain(&self) -> String {
+ self.iter()
+ .map(|(category, lines)| iter::once(category).chain(lines).join("\n"))
+ .join("\n\n")
+ }
+
+ /// Renders the contents of this [`LogBag`] as HTML.
+ ///
+ /// Useful for posting to Matrix chats.
+ pub fn render_html(&self) -> String {
+ self.iter()
+ .map(|(category, mut lines)| {
+ format!("<b>{}</b><br>\n{}", category, lines.join("<br>\n"))
+ })
+ .join("<br>\n<br>\n")
+ }
+
+ /// Renders the contents of this [`LogBag`] as Markdown.
+ ///
+ /// Useful for posting to Discord chats.
+ pub fn render_markdown(&self) -> String {
+ self.iter()
+ .map(|(category, mut lines)| format!("**{}**\n{}", category, lines.join("\n")))
+ .join("\n\n")
+ }
+}
+
+impl FromStr for LogBag {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let data = s
+ .trim()
+ .split("\n\n")
+ .map(|chunk| {
+ let mut lines = chunk.split('\n');
+ let category = lines.next().unwrap();
+ (
+ category.to_string(),
+ lines.map(ToString::to_string).collect::<Vec<_>>(),
+ )
+ })
+ .collect();
+ Ok(LogBag { data })
+ }
+}
+
+impl From<Vec<(String, Vec<String>)>> for LogBag {
+ fn from(data: Vec<(String, Vec<String>)>) -> Self {
+ LogBag { data }
+ }
+}
+
+/// A helper function to return the right emoji for a given log.
+pub 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 => "❓",
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn insert() {
+ let mut logbag = LogBag::new();
+ logbag.insert("cat 1", "line 1".to_string());
+ assert_eq!(logbag.categories().count(), 1);
+ logbag.insert("cat 1", "line 2".to_string());
+ assert_eq!(logbag.categories().count(), 1);
+ logbag.insert("cat 2", "line 1".to_string());
+ assert_eq!(logbag.categories().count(), 2);
+
+ assert_eq!(
+ logbag.categories().collect::<Vec<_>>(),
+ vec!["cat 1", "cat 2"]
+ );
+ }
+
+ #[test]
+ fn parse_single() {
+ let mut logbag = LogBag::new();
+ logbag.insert("cat 1", "line 1".to_string());
+ logbag.insert("cat 1", "line 2".to_string());
+
+ assert_eq!(
+ LogBag::parse_plain(
+ "\
+cat 1
+line 1
+line 2"
+ ),
+ Some(logbag)
+ );
+ }
+
+ #[test]
+ fn parse_multi() {
+ let mut logbag = LogBag::new();
+ logbag.insert("cat 1", "line 1".to_string());
+ logbag.insert("cat 1", "line 2".to_string());
+ logbag.insert("cat 2", "line 1".to_string());
+ logbag.insert("cat 2", "line 2".to_string());
+
+ assert_eq!(
+ LogBag::parse_plain(
+ "\
+cat 1
+line 1
+line 2
+
+cat 2
+line 1
+line 2"
+ ),
+ Some(logbag)
+ );
+ }
+
+ #[test]
+ fn render_plain_single() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category", "line 1".to_string());
+ logbag.insert("category", "line 2".to_string());
+
+ assert_eq!(
+ logbag.render_plain(),
+ "\
+category
+line 1
+line 2"
+ );
+ }
+
+ #[test]
+ fn render_plain_multi() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category 1", "line 1".to_string());
+ logbag.insert("category 1", "line 2".to_string());
+ logbag.insert("category 2", "enil 1".to_string());
+ logbag.insert("category 2", "enil 2".to_string());
+
+ assert_eq!(
+ logbag.render_plain(),
+ "\
+category 1
+line 1
+line 2
+
+category 2
+enil 1
+enil 2"
+ );
+ }
+
+ #[test]
+ fn render_html_single() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category", "line 1".to_string());
+ logbag.insert("category", "line 2".to_string());
+
+ assert_eq!(
+ logbag.render_html(),
+ "\
+<b>category</b><br>
+line 1<br>
+line 2"
+ );
+ }
+
+ #[test]
+ fn render_html_multi() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category 1", "line 1".to_string());
+ logbag.insert("category 1", "line 2".to_string());
+ logbag.insert("category 2", "enil 1".to_string());
+ logbag.insert("category 2", "enil 2".to_string());
+
+ assert_eq!(
+ logbag.render_html(),
+ "\
+<b>category 1</b><br>
+line 1<br>
+line 2<br>
+<br>
+<b>category 2</b><br>
+enil 1<br>
+enil 2"
+ );
+ }
+
+ #[test]
+ fn render_markdown_single() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category", "line 1".to_string());
+ logbag.insert("category", "line 2".to_string());
+
+ assert_eq!(
+ logbag.render_markdown(),
+ "\
+**category**
+line 1
+line 2"
+ );
+ }
+
+ #[test]
+ fn render_markdown_multi() {
+ let mut logbag = LogBag::new();
+ logbag.insert("category 1", "line 1".to_string());
+ logbag.insert("category 1", "line 2".to_string());
+ logbag.insert("category 2", "enil 1".to_string());
+ logbag.insert("category 2", "enil 2".to_string());
+
+ assert_eq!(
+ logbag.render_markdown(),
+ "\
+**category 1**
+line 1
+line 2
+
+**category 2**
+enil 1
+enil 2"
+ );
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 43c98b1..ab40d86 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,6 +21,7 @@ use categories::Categorizable;
mod config;
use config::Config;
mod discord;
+mod logbag;
mod matrix;
const DPS_REPORT_API: &str = "https://dps.report/uploadContent";
diff --git a/src/matrix.rs b/src/matrix.rs
index bd83a1f..ffd223d 100644
--- a/src/matrix.rs
+++ b/src/matrix.rs
@@ -1,11 +1,12 @@
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, Outcome};
+use evtclib::Log;
use log::{debug, info};
use tokio::runtime::Runtime;
@@ -37,6 +38,15 @@ impl From<config::Matrix> for MatrixUser {
}
}
+/// 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)?;
@@ -62,8 +72,9 @@ pub fn post_link(user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Resu
}
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);
+ 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?;
}
}
@@ -81,15 +92,15 @@ async fn find_message(
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);
+ 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(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)));
- }
+ 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)));
}
}
}
@@ -98,11 +109,12 @@ async fn find_message(
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 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 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
@@ -115,6 +127,9 @@ async fn post_new(client: &Client, room_id: &RoomId, log: &Log, link: &str) -> R
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,
@@ -139,149 +154,12 @@ async fn update_message(
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 => "❓",
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use std::io::Cursor;
- use evtclib::Compression;
-
- static ARKK_LOG: &[u8] = include_bytes!("../test/logs/arkk.zevtc");
-
- fn arkk_log() -> Log {
- evtclib::process_stream(Cursor::new(ARKK_LOG), Compression::Zip).unwrap()
- }
-
- #[test]
- fn test_insert_log_new_category() {
- let log = arkk_log();
- let old_text = "\
-Unknown
-x https://dps.report.example";
- let new_text = insert_log(old_text, &log, "foobar");
-
- assert_eq!("\
-Unknown
-x https://dps.report.example
-
-99 CM (Shattered Observatory)
-✅ foobar", new_text);
- }
-
- #[test]
- fn test_insert_log_appending() {
- let log = arkk_log();
- let old_text = "\
-99 CM (Shattered Observatory)
-x old log here";
- let new_text = insert_log(old_text, &log, "foobar");
- assert_eq!("\
-99 CM (Shattered Observatory)
-x old log here
-✅ foobar", new_text);
- }
-
- #[test]
- fn test_insert_log_multiple() {
- let log = arkk_log();
- let old_text = "\
-100 CM (Sunqua Peak)
-y some old log here
-
-99 CM (Shattered Observatory)
-x old log here
-
-98 CM (Nightmare)
-z raboof";
- let new_text = insert_log(old_text, &log, "foobar");
- assert_eq!("\
-100 CM (Sunqua Peak)
-y some old log here
-
-99 CM (Shattered Observatory)
-x old log here
-✅ foobar
-
-98 CM (Nightmare)
-z raboof", new_text);
- }
-
- #[test]
- fn test_htmlify_single() {
- let plain = "\
-Header
-Log 1
-Log 2";
- assert_eq!(htmlify(plain), "\
-<b>Header</b><br>
-Log 1<br>
-Log 2");
- }
-
- #[test]
- fn test_htmlify_multiple() {
- let plain = "\
-Header 1
-Log 1
-Log 2
-
-Header 2
-Log 3
-Log 4";
- assert_eq!(htmlify(plain), "\
-<b>Header 1</b><br>
-Log 1<br>
-Log 2<br>
-<br>
-<b>Header 2</b><br>
-Log 3<br>
-Log 4");
- }
-
+/// 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
}