2024-12-12 14:07:20 +01:00
|
|
|
use std::error::Error;
|
2024-12-20 09:38:32 +01:00
|
|
|
use std::io::Read;
|
2025-01-14 17:40:04 +01:00
|
|
|
use lazy_static::lazy_static;
|
2024-12-12 14:07:20 +01:00
|
|
|
|
|
|
|
use colored::Colorize;
|
2024-12-20 09:56:09 +01:00
|
|
|
use regex::Regex;
|
2024-12-12 14:07:20 +01:00
|
|
|
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 {
|
2025-01-14 17:40:04 +01:00
|
|
|
key: "room-id-1",
|
2024-12-12 14:07:20 +01:00
|
|
|
default: "!someLongRoomIdentifier:matrix.org",
|
|
|
|
description: "API Username associated with the bot account used for writing messages.",
|
|
|
|
},
|
|
|
|
CfgField::Default {
|
2025-01-14 17:40:04 +01:00
|
|
|
key: "room-id-2",
|
2024-12-12 14:07:20 +01:00
|
|
|
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,
|
2025-01-14 17:40:04 +01:00
|
|
|
room_id_1: String,
|
|
|
|
room_id_2: String,
|
2024-12-12 14:07:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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(
|
2025-01-14 17:40:04 +01:00
|
|
|
homeserver_url: &str, user_id: &str, access_token: &str, room_id_1: &str,
|
|
|
|
room_id_2: &str, is_dry_run: bool,
|
2024-12-12 14:07:20 +01:00
|
|
|
) -> 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(),
|
2025-01-14 17:40:04 +01:00
|
|
|
room_id_2: room_id_2.to_string(),
|
|
|
|
room_id_1: room_id_1.to_string(),
|
2024-12-12 14:07:20 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
2025-01-14 17:40:04 +01:00
|
|
|
verboseln!("url: {}\n", url.blue());
|
2024-12-12 14:07:20 +01:00
|
|
|
|
|
|
|
// 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);
|
2024-12-12 14:07:20 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-12-20 09:38:32 +01:00
|
|
|
// 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,
|
|
|
|
// )
|
|
|
|
// }
|
2024-12-12 14:07:20 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2025-01-14 17:40:04 +01:00
|
|
|
pub fn format_text_to_html(markdown: String) -> Result<String, Box<dyn Error>> {
|
|
|
|
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();
|
2024-12-20 09:38:32 +01:00
|
|
|
}
|
2024-12-20 09:56:09 +01:00
|
|
|
|
2025-01-14 17:40:04 +01:00
|
|
|
let mut html = markdown;
|
2024-12-20 09:56:09 +01:00
|
|
|
|
2025-01-14 17:40:04 +01:00
|
|
|
// Handle URLs first
|
|
|
|
html = MATRIX_URL.replace_all(&html, r"<a href='$1'>$1</a>").to_string();
|
2024-12-20 09:56:09 +01:00
|
|
|
|
2025-01-14 17:40:04 +01:00
|
|
|
// Basic formatting
|
|
|
|
html = MATRIX_BOLD.replace_all(&html, r"<b>$1</b>").to_string();
|
|
|
|
html = MATRIX_ITALIC.replace_all(&html, r"<i>$1</i>").to_string();
|
|
|
|
|
|
|
|
// Convert newlines to <br>
|
|
|
|
html = html.replace("\n", "<br>");
|
2024-12-20 09:56:09 +01:00
|
|
|
|
|
|
|
Ok(html)
|
|
|
|
}
|
2025-01-14 17:40:04 +01:00
|
|
|
pub fn pandoc_convert_text_to_md(markdown: String) -> Result<String, Box<dyn Error>> {
|
2024-12-20 09:38:32 +01:00
|
|
|
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>> {
|
2025-01-14 17:40:04 +01:00
|
|
|
let formatted_text = Self::format_text_to_html(text.to_string())?;
|
2024-12-20 09:38:32 +01:00
|
|
|
let content = HashMap::from([
|
2025-01-06 08:09:42 +01:00
|
|
|
("type", "m.room.message"),
|
2024-12-20 09:38:32 +01:00
|
|
|
("msgtype", "m.text"),
|
|
|
|
("body", text),
|
|
|
|
("format", "org.matrix.custom.html"),
|
|
|
|
("formatted_body", &formatted_text),
|
2025-01-14 17:40:04 +01:00
|
|
|
// ("m.mentions", "{}"),
|
2024-12-20 09:38:32 +01:00
|
|
|
]);
|
2024-12-12 14:07:20 +01:00
|
|
|
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());
|
2025-01-14 17:40:04 +01:00
|
|
|
verboseln!("room event:{}", &endpoint.green());
|
2024-12-12 14:07:20 +01:00
|
|
|
self.put(&endpoint, None, Some(content), false)
|
|
|
|
}
|
2025-01-14 17:40:04 +01:00
|
|
|
pub fn send_message_to_two_rooms(
|
|
|
|
&mut self, message: &str
|
2024-12-12 15:29:16 +01:00
|
|
|
) -> Result<(), Box<dyn Error>> {
|
2025-01-14 17:40:04 +01:00
|
|
|
self.send_room_message( &self.room_id_2.clone(), &message,)?;
|
|
|
|
self.send_room_message( &self.room_id_1.clone(), &message,)?;
|
2024-12-12 14:07:20 +01:00
|
|
|
Ok(())
|
|
|
|
}
|
2024-12-20 09:38:32 +01:00
|
|
|
|
2024-12-12 14:07:20 +01:00
|
|
|
}
|