diff --git a/Cargo.lock b/Cargo.lock index c41772e..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,13 +141,16 @@ dependencies = [ "clap", "futures", "matrix-sdk", + "regex", "serde", + "serde_regex", "serde_yaml", "systemd", "thiserror", "tokio", "tracing-subscriber", "url", + "void", ] [[package]] @@ -1438,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]] @@ -1773,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" @@ -2365,6 +2390,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..8004b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,16 @@ 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" 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..f95a07c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -100,15 +100,26 @@ impl BadNewsBot { const KEY_MESSAGE: &str = "MESSAGE"; if let Some(unit) = record.get(KEY_UNIT) { - if !self.config.units.contains(unit) { - return; + let unit_config = match self.config.units.iter().find(|u| &u.name == unit) { + Some(config) => config, + None => return, + }; + + let message = match record.get(KEY_MESSAGE) { + Some(msg) => msg, + None => return, + }; + + if let Some(filter) = &unit_config.filter { + if !filter.is_match(message) { + return; + } } - let message = record.get(KEY_MESSAGE); 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), diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ffa367b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,111 @@ +use matrix_sdk::identifiers::RoomId; +use regex::Regex; +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)] +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 + #[serde(deserialize_with = "list_of_units")] + pub units: Vec, +} + +/// Holds a single unit's configuration. +#[derive(Clone, Debug, Deserialize)] +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. + #[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); + +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 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, + 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)) +} 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();