use std::error::Error; use std::io::Read; use lazy_static::lazy_static; use colored::Colorize; use regex::Regex; 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-1", default: "!someLongRoomIdentifier:matrix.org", description: "API Username associated with the bot account used for writing messages.", }, CfgField::Default { key: "room-id-2", 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_1: String, room_id_2: 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_1: &str, room_id_2: &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_2: room_id_2.to_string(), room_id_1: room_id_1.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); verboseln!("url: {}\n", url.blue()); // 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 { // println!("These were the parameters: {}", params); 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 format_text_to_html(markdown: String) -> Result> { lazy_static! { static ref MATRIX_BOLD: Regex = Regex::new(r"\*\*(.+?)\*\*").unwrap(); static ref MATRIX_ITALIC: Regex = Regex::new(r"\*(.+?)\*").unwrap(); static ref MATRIX_URL: Regex = Regex::new(r"(https?://\S+?)(?:[.,])?(?:\s|$)").unwrap(); } let mut html = markdown; // Handle URLs first html = MATRIX_URL.replace_all(&html, r"$1").to_string(); // Basic formatting html = MATRIX_BOLD.replace_all(&html, r"$1").to_string(); html = MATRIX_ITALIC.replace_all(&html, r"$1").to_string(); // Convert newlines to
html = html.replace("\n", "
"); Ok(html) } pub fn pandoc_convert_text_to_md(markdown: String) -> Result> { let (output, errors, status) = crate::pipe( "pandoc", &mut ["--from", "markdown-auto_identifiers", "--to", "html5"], markdown, )?; if status.success() { println!("Resultat von Pandoc: {}", output); Ok(output) } else { Err(format!("Pandoc error, exit code {:?}\n{}", status, errors).into()) } } pub fn send_room_message( &mut self, room_id: &str, text: &str, ) -> Result> { let formatted_text = Self::format_text_to_html(text.to_string())?; let content = HashMap::from([ ("type", "m.room.message"), ("msgtype", "m.text"), ("body", text), ("format", "org.matrix.custom.html"), ("formatted_body", &formatted_text), // ("m.mentions", "{}"), ]); 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()); verboseln!("room event:{}", &endpoint.green()); self.put(&endpoint, None, Some(content), false) } pub fn send_message_to_two_rooms( &mut self, message: &str ) -> Result<(), Box> { self.send_room_message( &self.room_id_2.clone(), &message,)?; self.send_room_message( &self.room_id_1.clone(), &message,)?; Ok(()) } }