use std::error::Error; use std::io::Read; use colored::Colorize; 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); print!("url: {}", url.yellow()); // 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 pandoc_convert_md_to_html(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 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::pandoc_convert_md_to_html(text.to_string())?; let content = HashMap::from([ ("msgtype", "m.text"), ("body", text), ("format", "org.matrix.custom.html"), ("formatted_body", &formatted_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()); println!("room event:{}", &endpoint.red()); 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( &self.room_id_for_long_messages.clone(), &long_message,)?; self.send_room_message( &self.room_id_for_short_messages.clone(), &short_message,)?; Ok(()) } }