From f22416adacb867106ce8a02815ff04cd33b554c3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 10 Apr 2021 02:01:17 +0200 Subject: [PATCH 1/5] config: extract Config into its own file --- src/config.rs | 23 +++++++++++++++++++++++ src/main.rs | 24 ++---------------------- 2 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9d99a37 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,23 @@ +use matrix_sdk::identifiers::RoomId; +use serde::Deserialize; +use std::collections::HashSet; +use std::path::PathBuf; +use url::Url; + +/// Holds the configuration for the bot. +#[derive(Clone, Deserialize)] +pub struct Config { + /// The URL for the homeserver we should connect to + pub homeserver: Url, + /// The bot's account username + pub username: String, + /// The bot's account password + pub password: String, + /// Path to a directory where the bot will store Matrix state and current session information. + pub state_dir: PathBuf, + /// ID of the Matrix room where the bot should post messages. The bot will only accept + /// invitations to this room. + pub room_id: RoomId, + /// Units to watch for logs + pub units: HashSet, +} diff --git a/src/main.rs b/src/main.rs index dee3102..877d58e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,18 @@ use std::{ - collections::HashSet, fs::File, io::{self, BufReader}, path::PathBuf, }; use clap::Clap; -use matrix_sdk::identifiers::RoomId; -use serde::Deserialize; use thiserror::Error; -use url::Url; mod autojoin; mod bot; +mod config; use bot::BadNewsBot; +use config::Config; #[derive(Error, Debug)] enum BadNewsError { @@ -32,24 +30,6 @@ struct Opts { config: PathBuf, } -/// Holds the configuration for the bot. -#[derive(Clone, Deserialize)] -pub struct Config { - /// The URL for the homeserver we should connect to - homeserver: Url, - /// The bot's account username - username: String, - /// The bot's account password - password: String, - /// Path to a directory where the bot will store Matrix state and current session information. - state_dir: PathBuf, - /// ID of the Matrix room where the bot should post messages. The bot will only accept - /// invitations to this room. - room_id: RoomId, - /// Units to watch for logs - units: HashSet, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); From 54f67887aaeb9e2c697c2a6a85c885fbb3db72e2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 10 Apr 2021 02:01:18 +0200 Subject: [PATCH 2/5] config: add optional filter option for units This allows the user to write: ```yaml units: - foo.service - bar.service - name: baz.service filter: "^Error: .*$" ``` So a unit can be provided as a string, or as a map which contains both `name` and `filter`. --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/bot.rs | 2 +- src/config.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c41772e..e2fbccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,7 @@ dependencies = [ "tokio", "tracing-subscriber", "url", + "void", ] [[package]] @@ -2365,6 +2366,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 7fe7e51..eb687c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ serde_yaml = "0.8" serde = "1.0" systemd = "0.8" thiserror = "1.0" +void = "1" [dependencies.matrix-sdk] git = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/src/bot.rs b/src/bot.rs index b2a6387..aa97d32 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -100,7 +100,7 @@ impl BadNewsBot { const KEY_MESSAGE: &str = "MESSAGE"; if let Some(unit) = record.get(KEY_UNIT) { - if !self.config.units.contains(unit) { + if !self.config.units.iter().map(|u| &u.name).any(|name| name == unit) { return; } diff --git a/src/config.rs b/src/config.rs index 9d99a37..0a06c3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,12 @@ use matrix_sdk::identifiers::RoomId; -use serde::Deserialize; -use std::collections::HashSet; +use serde::de::{self, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; +use std::fmt; +use std::marker::PhantomData; use std::path::PathBuf; +use std::str::FromStr; use url::Url; +use void::Void; /// Holds the configuration for the bot. #[derive(Clone, Deserialize)] @@ -19,5 +23,71 @@ pub struct Config { /// invitations to this room. pub room_id: RoomId, /// Units to watch for logs - pub units: HashSet, + pub units: Vec, +} + +/// Holds a single unit's configuration. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(from = "SerializedUnit")] +pub struct Unit { + /// Can be serialized from a string only instead of a map. + pub name: String, + /// Regex to filter each line read from the unit's logs. + pub filter: Option, // FIXME: regex +} + +#[derive(Debug, Deserialize)] +#[serde(transparent)] +struct SerializedUnit(#[serde(deserialize_with = "unit_name_or_struct")] Unit); + +impl From for Unit { + fn from(s: SerializedUnit) -> Self { + s.0 + } +} + +impl FromStr for Unit { + type Err = Void; + + fn from_str(s: &str) -> Result { + Ok(Unit { + name: s.to_string(), + filter: None, + }) + } +} + +fn unit_name_or_struct<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + struct StringOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringOrStruct + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or map") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(FromStr::from_str(value).unwrap()) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + } + } + + deserializer.deserialize_any(StringOrStruct(PhantomData)) } From ad420dc8745f0d640073b50c88e0b667159eedbd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 10 Apr 2021 02:01:19 +0200 Subject: [PATCH 3/5] config: store and parse 'filter' as regex --- Cargo.lock | 24 ++++++++++++++++++++++++ Cargo.toml | 2 ++ src/config.rs | 14 ++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2fbccb..a615ff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -132,7 +141,9 @@ dependencies = [ "clap", "futures", "matrix-sdk", + "regex", "serde", + "serde_regex", "serde_yaml", "systemd", "thiserror", @@ -1439,7 +1450,10 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", + "thread_local", ] [[package]] @@ -1774,6 +1788,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index eb687c7..8004b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,11 @@ edition = "2018" anyhow = "1.0" clap = "3.0.0-beta.2" futures = "0.3" +regex = "1" tokio = { version = "1", features = [ "full" ] } tracing-subscriber = "0.2" url = { version = "2.2", features = [ "serde" ] } +serde_regex = "1" serde_yaml = "0.8" serde = "1.0" systemd = "0.8" diff --git a/src/config.rs b/src/config.rs index 0a06c3a..0a4e40b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use matrix_sdk::identifiers::RoomId; +use regex::Regex; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use std::fmt; @@ -27,15 +28,24 @@ pub struct Config { } /// Holds a single unit's configuration. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[derive(Clone, Debug, Deserialize)] #[serde(from = "SerializedUnit")] pub struct Unit { /// Can be serialized from a string only instead of a map. pub name: String, /// Regex to filter each line read from the unit's logs. - pub filter: Option, // FIXME: regex + #[serde(with = "serde_regex")] + pub filter: Option, } +impl PartialEq for Unit { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Unit {} + #[derive(Debug, Deserialize)] #[serde(transparent)] struct SerializedUnit(#[serde(deserialize_with = "unit_name_or_struct")] Unit); From 87604b38d3fdcc1eaf5146b1b40dde6d0298f221 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 10 Apr 2021 02:01:20 +0200 Subject: [PATCH 4/5] bot: apply service filters --- src/bot.rs | 13 ++++++++++--- src/config.rs | 10 +++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index aa97d32..d48801e 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -100,11 +100,18 @@ impl BadNewsBot { const KEY_MESSAGE: &str = "MESSAGE"; if let Some(unit) = record.get(KEY_UNIT) { - if !self.config.units.iter().map(|u| &u.name).any(|name| name == unit) { - return; - } + let unit_config = match self.config.units.iter().find(|u| &u.name == unit) { + Some(config) => config, + None => return, + }; let message = record.get(KEY_MESSAGE); + if let Some(filter) = &unit_config.filter { + if message.is_none() || !filter.is_match(message.unwrap()) { + return; + } + } + let message = format!( "[{}] {}", unit.strip_suffix(".service").unwrap_or(unit), diff --git a/src/config.rs b/src/config.rs index 0a4e40b..ffa367b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,12 +24,12 @@ pub struct Config { /// invitations to this room. pub room_id: RoomId, /// Units to watch for logs + #[serde(deserialize_with = "list_of_units")] pub units: Vec, } /// Holds a single unit's configuration. #[derive(Clone, Debug, Deserialize)] -#[serde(from = "SerializedUnit")] pub struct Unit { /// Can be serialized from a string only instead of a map. pub name: String, @@ -67,6 +67,14 @@ impl FromStr for Unit { } } +fn list_of_units<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let units: Vec = Deserialize::deserialize(deserializer)?; + Ok(units.into_iter().map(From::from).collect()) +} + fn unit_name_or_struct<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de> + FromStr, From 1e1dff9ab01c937cddf358a5b595e733831ff758 Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Sat, 10 Apr 2021 02:17:10 +0200 Subject: [PATCH 5/5] bot: don't bother processing empty messages --- src/bot.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index d48801e..f95a07c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -105,9 +105,13 @@ impl BadNewsBot { None => return, }; - let message = record.get(KEY_MESSAGE); + let message = match record.get(KEY_MESSAGE) { + Some(msg) => msg, + None => return, + }; + if let Some(filter) = &unit_config.filter { - if message.is_none() || !filter.is_match(message.unwrap()) { + if !filter.is_match(message) { return; } } @@ -115,7 +119,7 @@ impl BadNewsBot { let message = format!( "[{}] {}", unit.strip_suffix(".service").unwrap_or(unit), - message.map(|m| m.as_ref()).unwrap_or("") + message, ); let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text( TextMessageEventContent::plain(message),