From ec3a327c3ed2d8895efd2f901480ad8b4c7a8f89 Mon Sep 17 00:00:00 2001 From: murmeldin Date: Thu, 12 Dec 2024 14:07:20 +0100 Subject: [PATCH] new matrix.rs and improved mail messages --- src/lib.rs | 1 + src/main.rs | 58 ++++++++++++--- src/matrix.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 src/matrix.rs diff --git a/src/lib.rs b/src/lib.rs index 5f10aed..f9f4565 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod date; pub mod email; pub mod hedgedoc; pub mod key_value; +pub mod matrix; pub mod mediawiki; pub mod template; diff --git a/src/main.rs b/src/main.rs index f46a1b5..25b8408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use cccron_lib::email::{self, Email, SimpleEmail}; use cccron_lib::hedgedoc::{self, HedgeDoc}; use cccron_lib::is_dry_run; use cccron_lib::key_value::{KeyValueStore as KV, KeyValueStore}; +use cccron_lib::matrix::{self, MatrixClient}; use cccron_lib::mediawiki::{self, Mediawiki}; use cccron_lib::NYI; use cccron_lib::{trace_var, trace_var_, verboseln}; @@ -55,7 +56,7 @@ const CONFIG_SPEC: CfgSpec<'static> = CfgSpec { hedgedoc::CONFIG, mediawiki::CONFIG, email::CONFIG, - // TODO: Matrix, …? + matrix::CONFIG, CfgGroup { name: "text", description: "Various strings used.", fields: &[ @@ -470,40 +471,79 @@ fn do_protocol( wiki: &Mediawiki, ) -> Result<(), Box> { NYI!("trace/verbose annotations"); - let (current_pad_id, pad_content, toc, n_topics) = get_pad_info(config, hedgedoc); + let (current_pad_id, pad_content_without_cleanup, toc, n_topics) = get_pad_info(config, hedgedoc); if !toc.is_empty() { let human_date = plenum_day.format("%d.%m.%Y"); - let pad_content = hedgedoc::strip_metadata(pad_content); + let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone()); let subject = format!("Protokoll vom Plenum am {human_date}"); let pad_content = pad_content.replace("[toc]", &toc); let body = format!( "Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\ - Das Pad für das nächste Plenum ist zu finden unter <{}/{}>.\nDie Protokolle der letzten Plena findet ihr im wiki unter <{}/index.php?title={}>.\n\n---Protokoll:---\n{pad_content}\n-----", + Das Pad für das nächste Plenum ist zu finden unter <{}/{}>.\nDie Protokolle der letzten Plena findet ihr im wiki unter <{}/index.php?title={}>.\n\n---Protokoll:---\n{}\n-----", &config["hedgedoc-server-url"], &config["hedgedoc-next-id"], &config["wiki-server-url"], - &config["wiki-plenum-page"] + &config["wiki-plenum-page"], + pad_content.clone(), ); let _message_id = send_email(&subject, &body, email, config)?; mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?; config.set("state-name", &ProgramState::Logged.to_string()).ok(); } else { let human_date = plenum_day.format("%d.%m.%Y"); - let pad_content = hedgedoc::strip_metadata(pad_content); + let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone()); let subject = format!("Protokoll vom ausgefallenem Plenum am {human_date}"); let pad_content = pad_content.replace("[toc]", &toc); let body = format!( - "Das letzte Plenum hatte Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\ - Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n---Protokoll:---{pad_content}", + "Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\ + Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n---Protokoll:---{}", &config["hedgedoc-server-url"], &config["hedgedoc-next-id"], &config["wiki-server-url"], - &config["wiki-plenum-page"] + &config["wiki-plenum-page"], + pad_content.clone(), ); let _message_id = send_email(&subject, &body, email, config)?; mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?; config.set("state-name", &ProgramState::Logged.to_string()).ok(); } + let mut matrix = MatrixClient::new( + &config["matrix-homeserver-url"], + &config["matrix-user-id"], + &config["matrix-access-token"], + "!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-short-messages"], + "!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-long-messages"], + is_dry_run(), + ); + // Send the matrix room message + let human_date = plenum_day.format("%d.%m.%Y"); + let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup); + let pad_content = pad_content.replace("[toc]", &toc); + let long_message = format!( + "Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\ + Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n", + &config["hedgedoc-server-url"], + &config["hedgedoc-next-id"], + &config["wiki-server-url"], + &config["wiki-plenum-page"], + ); + let full_long_message = format!( + "{}\n\n{}\n\n{}", + &config["text-email-greeting"], long_message, &config["text-email-signature"] + ); + let short_message = format!( + "Das letzte Plenum hatte Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\ + Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n", + &config["hedgedoc-server-url"], + &config["hedgedoc-next-id"], + &config["wiki-server-url"], + &config["wiki-plenum-page"] + ); + let full_short_message = format!( + "{}\n\n{}\n\n{}", + &config["text-email-greeting"], short_message, &config["text-email-signature"] + ); + matrix.send_short_and_long_messages_to_two_rooms(&full_short_message, &full_long_message)?; Ok(()) } diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..688a783 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,194 @@ +use std::cell::OnceCell; +use std::error::Error; + +use clap::builder::Str; +use colored::Colorize; + +use nom::Err; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use reqwest::blocking::Client; +use serde_json::{json, Value}; + +use crate::config_spec::{CfgField, CfgGroup}; +use crate::{trace_var, verboseln}; + +pub const CONFIG: CfgGroup<'static> = CfgGroup { + name: "matrix", + description: "API Settings for matrix", + fields: &[ + CfgField::Default { + key: "homeserver-url", + default: "https://matrix-client.matrix.org", + description: "Homeserver where the bot logs in.", + }, + CfgField::Default { + key: "user-id", + default: "@bot_username:matrix.org", + description: "API Username associated with the bot account used for writing messages.", + }, + CfgField::Password { + key: "access-token", + description: "Access Token / \"password\" used for authenticating as the bot.", + }, + CfgField::Default { + key: "room-id-for-long-messages", + default: "!someLongRoomIdentifier:matrix.org", + description: "API Username associated with the bot account used for writing messages.", + }, + CfgField::Default { + key: "room-id-for-short-messages", + default: "!someLongRoomIdentifier:matrix.org", + description: "API Username associated with the bot account used for writing messages.", + }, + ], +}; + +pub struct MatrixClient { + homeserver_url: String, + user_id: String, + access_token: String, + is_dry_run: bool, + client: Client, + txn_id: u64, + room_id_for_short_messages: String, + room_id_for_long_messages: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoginRequest { + user: String, + password: String, + #[serde(rename = "type")] + login_type: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoginResponse { + access_token: String, +} + +impl std::fmt::Debug for MatrixClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("matrix") + .field("homeserver_url", &self.homeserver_url) + .field("user_id", &self.user_id) + .field("access_token", &"*****") + .field("is_dry_run", &self.is_dry_run) + .field("client", &self.client) + .finish() + } +} + +impl MatrixClient { + pub fn new( + homeserver_url: &str, user_id: &str, access_token: &str, room_id_for_short_messages: &str, room_id_for_long_messages: &str, is_dry_run: bool, + ) -> Self { + Self { + homeserver_url: homeserver_url.to_string(), + user_id: user_id.to_string(), + access_token: access_token.to_string(), + is_dry_run, + client: Client::builder().cookie_store(true).build().unwrap(), + txn_id: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + room_id_for_long_messages: room_id_for_long_messages.to_string(), + room_id_for_short_messages: room_id_for_short_messages.to_string(), + } + } + fn request( + &self, method: reqwest::Method, endpoint: &str, + query_data: Option<&HashMap>, json_data: Option<&T>, unauth: bool, + ) -> Result> { + let client = reqwest::blocking::Client::new(); + let url = format!("{}/_matrix/client{}", self.homeserver_url, endpoint); + + // Construct URL with query parameters + let mut request = client.request(method, &url); + if let Some(params) = query_data { + request = request.query(params); + } + + // Add JSON body if provided + if let Some(data) = json_data { + request = request.json(data); + } + + // Add authorization header if not unauthenticated request + if !unauth { + request = request.header("Authorization", format!("Bearer {}", self.access_token)); + } + + let response = request.send()?; + + if response.status().is_success() { + Ok(response.json()?) + } else { + Err(format!("Request failed: {}", response.status()).into()) + } + } + + fn put( + &self, endpoint: &str, query_data: Option<&HashMap>, json_data: Option<&T>, + unauth: bool, + ) -> Result> { + self.request(reqwest::Method::PUT, endpoint, query_data, json_data, unauth) + } + + fn post( + &self, endpoint: &str, query_data: Option<&HashMap>, json_data: Option<&T>, + unauth: bool, + ) -> Result> { + self.request(reqwest::Method::POST, endpoint, query_data, json_data, unauth) + } + + fn get( + &self, endpoint: &str, query_data: Option<&HashMap>, unauth: bool, + ) -> Result> { + self.request::>( + reqwest::Method::GET, + endpoint, + query_data, + None, + unauth, + ) + } + + pub fn login(&mut self, username: &str, password: &str) -> Result<(), Box> { + let login_request = LoginRequest { + user: username.to_string(), + password: password.to_string(), + login_type: "m.login.password".to_string(), + }; + + let response: LoginResponse = + serde_json::from_value(self.post("/r0/login", None, Some(&login_request), true)?)?; + + self.access_token = response.access_token; + Ok(()) + } + + fn txn_id(&mut self) -> u64 { + let current = self.txn_id; + self.txn_id += 1; + current + } + + pub fn send_room_message(&mut self, room_id: &str, text: &str) -> Result> { + let content = HashMap::from([("msgtype", "m.text"), ("body", text)]); + self.send_room_event(&room_id, "m.room.message", &content) + } + + fn send_room_event( + &mut self, room: &str, event_type: &str, content: &impl Serialize, + ) -> Result> { + let endpoint = format!("/r0/rooms/{}/send/{}/{}", room, event_type, self.txn_id()); + self.put(&endpoint, None, Some(content), false) + } + pub fn send_short_and_long_messages_to_two_rooms(&mut self, short_message: &str, long_message: &str) -> Result<(), Box> { + self.send_room_message(&long_message, &self.room_id_for_long_messages.clone())?; + self.send_room_message(&short_message, &self.room_id_for_short_messages.clone())?; + Ok(()) + } +}