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(()) } }