plenum-bot/src/matrix.rs

258 lines
8.7 KiB
Rust
Raw Normal View History

use std::error::Error;
use std::io::Read;
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-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(
2024-12-12 15:29:16 +01:00
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<T: Serialize>(
&self, method: reqwest::Method, endpoint: &str,
query_data: Option<&HashMap<String, String>>, json_data: Option<&T>, unauth: bool,
) -> Result<Value, Box<dyn Error>> {
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 {
2025-01-06 08:09:42 +01:00
// println!("These were the parameters: {}", params);
Err(format!("Request failed: {}", response.status()).into())
}
}
fn put<T: Serialize>(
&self, endpoint: &str, query_data: Option<&HashMap<String, String>>, json_data: Option<&T>,
unauth: bool,
) -> Result<Value, Box<dyn Error>> {
self.request(reqwest::Method::PUT, endpoint, query_data, json_data, unauth)
}
fn post<T: Serialize>(
&self, endpoint: &str, query_data: Option<&HashMap<String, String>>, json_data: Option<&T>,
unauth: bool,
) -> Result<Value, Box<dyn Error>> {
self.request(reqwest::Method::POST, endpoint, query_data, json_data, unauth)
}
// fn get(
// &self, endpoint: &str, query_data: Option<&HashMap<String, String>>, unauth: bool,
// ) -> Result<Value, Box<dyn Error>> {
// self.request::<HashMap<String, String>>(
// reqwest::Method::GET,
// endpoint,
// query_data,
// None,
// unauth,
// )
// }
pub fn login(&mut self, username: &str, password: &str) -> Result<(), Box<dyn Error>> {
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<String, Box<dyn Error>> {
let (output, errors, status) = crate::pipe(
"pandoc",
&mut [
"--from", "markdown-auto_identifiers",
"--to", "html5",
"--wrap=none", // Verhindert Zeilenumbrüche
"--no-highlight", // Deaktiviert Syntax-Highlighting
],
markdown,
)?;
if status.success() {
println!("Resultat von Pandoc: {}", output);
Ok(output)
} else {
Err(format!("Pandoc error, exit code {:?}\n{}", status, errors).into())
}
}
pub fn format_matrix_html(markdown: String) -> Result<String, Box<dyn Error>> {
let mut html = Self::pandoc_convert_md_to_html(markdown)?;
let url_regex = Regex::new(r"(https?://[^\s<]+)")?;
html = url_regex.replace_all(&html, r#"<a href="$1">$1</a>"#).to_string();
html = html.replace("\n\n", "</p><p>");
html = html.replace("\n", "<br>");
html = html.replace("**", "<strong>");
html = html.replace("__", "<strong>");
Ok(html)
}
pub fn pandoc_convert_text_to_md(markdown: String) -> Result<String, Box<dyn Error>> {
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())
}
}
2024-12-12 15:29:16 +01:00
pub fn send_room_message(
&mut self, room_id: &str, text: &str,
) -> Result<Value, Box<dyn Error>> {
let formatted_text = Self::format_matrix_html(text.to_string())?;
let content = HashMap::from([
2025-01-06 08:09:42 +01:00
("type", "m.room.message"),
("msgtype", "m.text"),
("body", text),
("format", "org.matrix.custom.html"),
("formatted_body", &formatted_text),
2025-01-06 08:09:42 +01:00
("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<Value, Box<dyn Error>> {
let endpoint = format!("/r0/rooms/{}/send/{}/{}", room, event_type, self.txn_id());
println!("room event:{}", &endpoint.red());
self.put(&endpoint, None, Some(content), false)
}
2024-12-12 15:29:16 +01:00
pub fn send_short_and_long_messages_to_two_rooms(
&mut self, short_message: &str, long_message: &str,
) -> Result<(), Box<dyn Error>> {
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(())
}
}