Compare commits
2 commits
7023e9445f
...
ec3a327c3e
Author | SHA1 | Date | |
---|---|---|---|
ec3a327c3e | |||
81d7282e8a |
|
@ -3,6 +3,7 @@ pub mod date;
|
|||
pub mod email;
|
||||
pub mod hedgedoc;
|
||||
pub mod key_value;
|
||||
pub mod matrix;
|
||||
pub mod mediawiki;
|
||||
pub mod template;
|
||||
|
||||
|
|
58
src/main.rs
58
src/main.rs
|
@ -13,6 +13,7 @@ use cccron_lib::email::{self, Email, SimpleEmail};
|
|||
use cccron_lib::hedgedoc::{self, HedgeDoc};
|
||||
use cccron_lib::is_dry_run;
|
||||
use cccron_lib::key_value::{KeyValueStore as KV, KeyValueStore};
|
||||
use cccron_lib::matrix::{self, MatrixClient};
|
||||
use cccron_lib::mediawiki::{self, Mediawiki};
|
||||
use cccron_lib::NYI;
|
||||
use cccron_lib::{trace_var, trace_var_, verboseln};
|
||||
|
@ -55,7 +56,7 @@ const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
|
|||
hedgedoc::CONFIG,
|
||||
mediawiki::CONFIG,
|
||||
email::CONFIG,
|
||||
// TODO: Matrix, …?
|
||||
matrix::CONFIG,
|
||||
CfgGroup { name: "text",
|
||||
description: "Various strings used.",
|
||||
fields: &[
|
||||
|
@ -470,40 +471,79 @@ fn do_protocol(
|
|||
wiki: &Mediawiki,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
NYI!("trace/verbose annotations");
|
||||
let (current_pad_id, pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
|
||||
let (current_pad_id, pad_content_without_cleanup, toc, n_topics) = get_pad_info(config, hedgedoc);
|
||||
if !toc.is_empty() {
|
||||
let human_date = plenum_day.format("%d.%m.%Y");
|
||||
let pad_content = hedgedoc::strip_metadata(pad_content);
|
||||
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone());
|
||||
let subject = format!("Protokoll vom Plenum am {human_date}");
|
||||
let pad_content = pad_content.replace("[toc]", &toc);
|
||||
let body = format!(
|
||||
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
|
||||
Das Pad für das nächste Plenum ist zu finden unter <{}/{}>.\nDie Protokolle der letzten Plena findet ihr im wiki unter <{}/index.php?title={}>.\n\n---Protokoll:---\n{pad_content}\n-----",
|
||||
Das Pad für das nächste Plenum ist zu finden unter <{}/{}>.\nDie Protokolle der letzten Plena findet ihr im wiki unter <{}/index.php?title={}>.\n\n---Protokoll:---\n{}\n-----",
|
||||
&config["hedgedoc-server-url"],
|
||||
&config["hedgedoc-next-id"],
|
||||
&config["wiki-server-url"],
|
||||
&config["wiki-plenum-page"]
|
||||
&config["wiki-plenum-page"],
|
||||
pad_content.clone(),
|
||||
);
|
||||
let _message_id = send_email(&subject, &body, email, config)?;
|
||||
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
|
||||
config.set("state-name", &ProgramState::Logged.to_string()).ok();
|
||||
} else {
|
||||
let human_date = plenum_day.format("%d.%m.%Y");
|
||||
let pad_content = hedgedoc::strip_metadata(pad_content);
|
||||
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone());
|
||||
let subject = format!("Protokoll vom ausgefallenem Plenum am {human_date}");
|
||||
let pad_content = pad_content.replace("[toc]", &toc);
|
||||
let body = format!(
|
||||
"Das letzte Plenum hatte Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
|
||||
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n---Protokoll:---{pad_content}",
|
||||
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
|
||||
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n---Protokoll:---{}",
|
||||
&config["hedgedoc-server-url"],
|
||||
&config["hedgedoc-next-id"],
|
||||
&config["wiki-server-url"],
|
||||
&config["wiki-plenum-page"]
|
||||
&config["wiki-plenum-page"],
|
||||
pad_content.clone(),
|
||||
);
|
||||
let _message_id = send_email(&subject, &body, email, config)?;
|
||||
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
|
||||
config.set("state-name", &ProgramState::Logged.to_string()).ok();
|
||||
}
|
||||
let mut matrix = MatrixClient::new(
|
||||
&config["matrix-homeserver-url"],
|
||||
&config["matrix-user-id"],
|
||||
&config["matrix-access-token"],
|
||||
"!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-short-messages"],
|
||||
"!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-long-messages"],
|
||||
is_dry_run(),
|
||||
);
|
||||
// Send the matrix room message
|
||||
let human_date = plenum_day.format("%d.%m.%Y");
|
||||
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup);
|
||||
let pad_content = pad_content.replace("[toc]", &toc);
|
||||
let long_message = format!(
|
||||
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
|
||||
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n",
|
||||
&config["hedgedoc-server-url"],
|
||||
&config["hedgedoc-next-id"],
|
||||
&config["wiki-server-url"],
|
||||
&config["wiki-plenum-page"],
|
||||
);
|
||||
let full_long_message = format!(
|
||||
"{}\n\n{}\n\n{}",
|
||||
&config["text-email-greeting"], long_message, &config["text-email-signature"]
|
||||
);
|
||||
let short_message = format!(
|
||||
"Das letzte Plenum hatte Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
|
||||
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n",
|
||||
&config["hedgedoc-server-url"],
|
||||
&config["hedgedoc-next-id"],
|
||||
&config["wiki-server-url"],
|
||||
&config["wiki-plenum-page"]
|
||||
);
|
||||
let full_short_message = format!(
|
||||
"{}\n\n{}\n\n{}",
|
||||
&config["text-email-greeting"], short_message, &config["text-email-signature"]
|
||||
);
|
||||
matrix.send_short_and_long_messages_to_two_rooms(&full_short_message, &full_long_message)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
194
src/matrix.rs
Normal file
194
src/matrix.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
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<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);
|
||||
|
||||
// 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<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 send_room_message(&mut self, room_id: &str, text: &str) -> Result<Value, Box<dyn Error>> {
|
||||
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<Value, Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use std::cell::OnceCell;
|
||||
use std::error::Error;
|
||||
use std::f64::consts::E;
|
||||
|
||||
use chrono::{Datelike, NaiveDate, Utc};
|
||||
use colored::Colorize;
|
||||
|
@ -75,13 +76,13 @@ impl std::fmt::Debug for Mediawiki {
|
|||
pub enum ValidRequestTypes {
|
||||
Get,
|
||||
Post,
|
||||
PostForEditing
|
||||
PostForEditing,
|
||||
}
|
||||
|
||||
pub enum ValidPageEdits {
|
||||
WithPotentiallyOverriding,
|
||||
WithoutOverriding,
|
||||
ModifyPlenumPageAfterwards_WithoutOverriding
|
||||
ModifyPlenumPageAfterwards_WithoutOverriding,
|
||||
}
|
||||
|
||||
impl Mediawiki {
|
||||
|
@ -223,7 +224,6 @@ impl Mediawiki {
|
|||
resp
|
||||
}
|
||||
|
||||
|
||||
/// Creates a completely new wiki page with page_content and page_title as inputs
|
||||
pub fn new_wiki_page(
|
||||
&self, page_title: &str, page_content: &str, update_main_page: ValidPageEdits,
|
||||
|
@ -253,9 +253,9 @@ impl Mediawiki {
|
|||
("title", page_title),
|
||||
("text", page_content),
|
||||
("token", self.csrf_token.get().unwrap()),
|
||||
("bot", "true")
|
||||
("bot", "true"),
|
||||
])
|
||||
}
|
||||
},
|
||||
ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding => {
|
||||
// This means we *CREATE* a *new Page* and always prevent overwriting
|
||||
Box::from([
|
||||
|
@ -267,7 +267,7 @@ impl Mediawiki {
|
|||
("createonly", "true"), // Prevent overwriting existing pages
|
||||
("bot", "true"),
|
||||
])
|
||||
}
|
||||
},
|
||||
ValidPageEdits::WithoutOverriding => {
|
||||
// This means we *CREATE* a *new Page* and always prevent overwriting
|
||||
Box::from([
|
||||
|
@ -279,7 +279,7 @@ impl Mediawiki {
|
|||
("createonly", "true"), // Prevent overwriting existing pages
|
||||
("bot", "true"),
|
||||
])
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
verboseln!("Current page title: {page_title}");
|
||||
|
@ -294,11 +294,19 @@ impl Mediawiki {
|
|||
let response_result = serde_json::from_str::<serde_json::Value>(&request_result);
|
||||
let response = response_result.unwrap_or_else(|e| {
|
||||
print!("Error while creating new wiki page:\n{}", e.to_string().cyan());
|
||||
return serde_json::from_str("\"(error)\"").unwrap()
|
||||
return serde_json::from_str("\"(error)\"").unwrap();
|
||||
});
|
||||
verboseln!("pos2");
|
||||
// Check if the page creation was successful
|
||||
if let Some(edit) = response.get("edit") {
|
||||
if let Some(error) = response.get("error") {
|
||||
if let Some(info) = error.get("info") {
|
||||
if info == "The page you tried to create has been created already." {
|
||||
verboseln!("The page you tried to create has been created already. Continuing...")
|
||||
} else {
|
||||
println!("There was an error while editing the wiki: {}", info.to_string().yellow())
|
||||
}
|
||||
}
|
||||
} else if let Some(edit) = response.get("edit") {
|
||||
verboseln!("pos3");
|
||||
if edit.get("result").and_then(|r| r.as_str()) == Some("Success") {
|
||||
verboseln!("Successfully created wiki page: {}", page_title);
|
||||
|
@ -318,9 +326,9 @@ impl Mediawiki {
|
|||
verboseln!("updating main page...");
|
||||
self.update_plenum_page(page_title)?
|
||||
},
|
||||
_ => ()
|
||||
_ => (),
|
||||
};
|
||||
return Ok(page_title.to_string())
|
||||
return Ok(page_title.to_string());
|
||||
}
|
||||
|
||||
/// This function is responsible for updating the main plenum page:
|
||||
|
@ -339,7 +347,6 @@ impl Mediawiki {
|
|||
|
||||
// check first if the script has been run before and if the link already exists
|
||||
if !page_content.contains(&new_page_title_to_link_to) {
|
||||
|
||||
// check if the current year heading pattern exists
|
||||
if !page_content.contains(&year_heading_pattern) {
|
||||
// If not, add a new year heading pattern
|
||||
|
@ -375,7 +382,11 @@ impl Mediawiki {
|
|||
verboseln!("{}", "The bot appears to have been run before and a duplicate link to the new plenum pad was avoided.".yellow())
|
||||
}
|
||||
// refresh page
|
||||
self.new_wiki_page(&self.plenum_main_page_name, &page_content, ValidPageEdits::WithPotentiallyOverriding)?;
|
||||
self.new_wiki_page(
|
||||
&self.plenum_main_page_name,
|
||||
&page_content,
|
||||
ValidPageEdits::WithPotentiallyOverriding,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
/// This function downloads and returns the contents of a wiki page when given the page's title (e.g. `page_title = Plenum/13._August_2024`)
|
||||
|
@ -512,7 +523,11 @@ pub fn pad_ins_wiki(
|
|||
let page_title = create_page_title(date);
|
||||
let full_page_title = format!("{}/{}", wiki.plenum_main_page_name, page_title);
|
||||
|
||||
wiki.new_wiki_page(&full_page_title, &pad_converted, ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding)?;
|
||||
wiki.new_wiki_page(
|
||||
&full_page_title,
|
||||
&pad_converted,
|
||||
ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding,
|
||||
)?;
|
||||
|
||||
verboseln!("Finished successfully with wiki");
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue