mediawiki finally working properly, e-mail messages fixed, ran rustfmt

This commit is contained in:
murmeldin 2024-12-11 16:07:25 +01:00
parent 9583d9c37d
commit 7023e9445f
3 changed files with 256 additions and 165 deletions

View file

@ -59,19 +59,26 @@ impl HedgeDoc {
if response.status().is_success() {
Ok(response)
} else {
Err(format!("Failed to connect to hedgedoc server: HTTP status code {}", response.status()).into())
}
Err(format!(
"Failed to connect to hedgedoc server: HTTP status code {}",
response.status()
)
.into())
}
},
Err(e) => {
if e.is_connect() {
Err("Failed to connect to hedgedoc server. Please check your internet connection or the server URL.".into())
} else {
Err(format!("An error occurred while sending the request to the hedgedoc server: {}", e).into())
Err(format!(
"An error occurred while sending the request to the hedgedoc server: {}",
e
)
.into())
}
},
}
}
}
}
fn get_id_from_response(&self, res: Response) -> String {
res.url().to_string().trim_start_matches(&format!("{}/", self.server_url)).to_string()
@ -122,7 +129,8 @@ pub fn strip_metadata(pad_content: String) -> String {
let re_yaml = Regex::new(r"(?s)---\s*.*?\s*(?:\.\.\.|---)").unwrap();
let pad_content = re_yaml.replace_all(&pad_content, "").to_string();
let re_comment = Regex::new(r"(?s)<!--.*?-->").unwrap();
re_comment.replace_all(&pad_content, "").to_string()
let content_without_comments = re_comment.replace_all(&pad_content, "").to_string();
content_without_comments.trim().to_string()
}
pub fn summarize(pad_content: String) -> String {

View file

@ -7,11 +7,6 @@ use std::io::IsTerminal;
use std::os::linux::raw::stat;
use std::time::Instant;
use chrono::{Local, NaiveDate, Utc, DateTime};
use clap::{Arg, Command};
use colored::Colorize;
use regex::Regex;
use cccron_lib::{trace_var, trace_var_, verboseln};
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
use cccron_lib::date;
use cccron_lib::email::{self, Email, SimpleEmail};
@ -20,6 +15,11 @@ use cccron_lib::is_dry_run;
use cccron_lib::key_value::{KeyValueStore as KV, KeyValueStore};
use cccron_lib::mediawiki::{self, Mediawiki};
use cccron_lib::NYI;
use cccron_lib::{trace_var, trace_var_, verboseln};
use chrono::{DateTime, Local, NaiveDate, Utc};
use clap::{Arg, Command};
use colored::Colorize;
use regex::Regex;
/* ***** Config Spec ***** */
const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
@ -98,7 +98,7 @@ fn today() -> NaiveDate {
fn state(config: &KeyValueStore) -> ProgramState {
match env::var("STATE_OVERRIDE") {
Ok(val) => ProgramState::parse(&val),
Err(_e) => ProgramState::parse(&config["state-name"])
Err(_e) => ProgramState::parse(&config["state-name"]),
}
}
@ -475,20 +475,34 @@ fn do_protocol(
let human_date = plenum_day.format("%d.%m.%Y");
let pad_content = hedgedoc::strip_metadata(pad_content);
let subject = format!("Protokoll vom Plenum am {human_date}");
NYI!("link for next plenum");
NYI!("replace [toc] with actual table of contents");
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 {}.\n\n-----\n\n{pad_content}",
"<FIXME>"
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-----",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"]
);
let _message_id = send_email(&subject, &body, email, config)?;
NYI!("convert to mediawiki");
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
NYI!("add to wiki");
config.set("state-name", &ProgramState::Logged.to_string()).ok();
} else {
NYI!("What do we do in the no topics / no plenum case?");
let human_date = plenum_day.format("%d.%m.%Y");
let pad_content = hedgedoc::strip_metadata(pad_content);
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}",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"]
);
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();
}
Ok(())
}
@ -683,5 +697,5 @@ fn display_logo(eta: &str) {

"#;
let ansi_art = format!("{ansi_art_pt1}{eta}{ansi_art_pt2}");
println!("{}", ansi_art );
println!("{}", ansi_art);
}

View file

@ -7,8 +7,8 @@ use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::{trace_var, verboseln};
use crate::config_spec::{CfgField, CfgGroup};
use crate::{trace_var, verboseln};
pub const CONFIG: CfgGroup<'static> = CfgGroup {
name: "wiki",
@ -24,10 +24,7 @@ pub const CONFIG: CfgGroup<'static> = CfgGroup {
default: "cccb-wiki",
description: "HTTP basic auth user name.",
},
CfgField::Password {
key: "http-password",
description: "HTTP basic auth password."
},
CfgField::Password { key: "http-password", description: "HTTP basic auth password." },
CfgField::Default {
key: "api-user",
default: "PlenumBot@PlenumBot-PW2",
@ -45,8 +42,8 @@ pub const CONFIG: CfgGroup<'static> = CfgGroup {
CfgField::Default {
key: "eta",
default: "no ETA, program never ran",
description: "ETA message for estimating time the program takes."
}
description: "ETA message for estimating time the program takes.",
},
],
};
@ -81,9 +78,16 @@ pub enum ValidRequestTypes {
PostForEditing
}
pub enum ValidPageEdits {
WithPotentiallyOverriding,
WithoutOverriding,
ModifyPlenumPageAfterwards_WithoutOverriding
}
impl Mediawiki {
pub fn new(
server_url: &str, http_auth_user: &str, http_auth_password: &str, api_user: &str, api_secret: &str, is_dry_run: bool, plenum_main_page_name: &str,
server_url: &str, http_auth_user: &str, http_auth_password: &str, api_user: &str,
api_secret: &str, is_dry_run: bool, plenum_main_page_name: &str,
) -> Self {
Self {
server_url: server_url.to_string(),
@ -98,14 +102,14 @@ impl Mediawiki {
client: Client::builder().cookie_store(true).build().unwrap(),
}
}
pub fn login (&self) -> Result<(), Box<dyn Error>> {
pub fn login(&self) -> Result<(), Box<dyn Error>> {
let url = format!("{}/api.php?", self.server_url);
// retrieve login token first
let params_0: Box<[(&str, &str)]> = Box::from([
("action", "query"),
("meta", "tokens"),
("type", "login"),
("format", "json")
("format", "json"),
]);
verboseln!("Login params: {:?}", params_0);
let resp_0: String = self.make_request(url.clone(), params_0, ValidRequestTypes::Get)?;
@ -116,10 +120,11 @@ impl Mediawiki {
let login_token = resp_0_deserialized
.pointer("/query/tokens/logintoken")
.and_then(|v| v.as_str())
.map(|token| token.replace("+\\", ""))
.ok_or("Login token not found")?;
self.login_token.set(login_token)?;
verboseln!("Value of login token: {login_token}");
self.login_token.set(login_token.to_string())?;
verboseln!("login0 finished");
let login_token: String = self.login_token.clone().into_inner().unwrap();
@ -129,10 +134,11 @@ impl Mediawiki {
("lgname", &self.api_user),
("lgpassword", &self.api_secret),
("lgtoken", &login_token),
("format", "json")
("format", "json"),
]);
verboseln!("Login params: {:?}", params_1);
let resp_1: String = self.client
let resp_1: String = self
.client
.post(&url)
// .basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
.form(&params_1)
@ -151,13 +157,9 @@ impl Mediawiki {
}
}
pub fn get_csrf_token(&self) -> Result<(), Box<dyn Error>> {
let url =
format!("{}/api.php?", self.server_url);
let params: Box<[(&str, &str)]> = Box::from([
("format", "json"),
("meta", "tokens"),
("action", "query")
]);
let url = format!("{}/api.php?", self.server_url);
let params: Box<[(&str, &str)]> =
Box::from([("format", "json"), ("meta", "tokens"), ("action", "query")]);
let resp: String = self.make_request(url, params, ValidRequestTypes::Get)?;
verboseln!("Raw response csrf: {}", resp);
let response_deserialized: QueryResponseCsrf = serde_json::from_str(&resp)?;
@ -171,56 +173,67 @@ impl Mediawiki {
Ok(())
}
pub fn make_request(&self, url: String, params: Box<[(&str, &str)]>, request_type: ValidRequestTypes) -> Result<String, Box<dyn Error>> {
let resp: Result<String, Box<dyn Error>> = match
match request_type {
pub fn make_request(
&self, url: String, params: Box<[(&str, &str)]>, request_type: ValidRequestTypes,
) -> Result<String, Box<dyn Error>> {
let resp: Result<String, Box<dyn Error>> = match match request_type {
ValidRequestTypes::Get => {
self
.client
self.client
.get(url)
//.basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
.query(&params)
.send()
}
},
ValidRequestTypes::Post | ValidRequestTypes::PostForEditing => {
// convert the params into a HashMap for JSON
let params_map: std::collections::HashMap<_, _> = params.iter().cloned().collect();
self
.client
self.client
.post(url)
//.basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
.query(&params_map)
.form(&params_map)
.send()
}
}
{
},
} {
Ok(response) => {
if response.status().is_success() {
match request_type {
ValidRequestTypes::PostForEditing => Ok(response.text()?),
_ => Ok(response.text()?)
}
}
else {
Err(format!("Failed to connect to wiki server: HTTP status code {}", response.status()).into())
_ => Ok(response.text()?),
}
} else {
Err(format!(
"Failed to connect to wiki server: HTTP status code {}",
response.status()
)
.into())
}
},
Err(e) => {
if e.is_connect() {
Err(format!("Failed to connect to wiki server. Please check your internet connection or the server URL.\n(Error: {})", e).into())
} else {
Err(format!("An error occurred while sending the request to the wiki server: {}", e).into())
}
Err(format!(
"An error occurred while sending the request to the wiki server: {}",
e
)
.into())
}
},
};
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: bool) -> Result<String, Box<dyn Error>> {
pub fn new_wiki_page(
&self, page_title: &str, page_content: &str, update_main_page: ValidPageEdits,
) -> Result<String, Box<dyn Error>> {
// Prevent dry run from making actual wiki edits
if self.is_dry_run {
println!("Dry run: Would create wiki page '{}' with content {}", page_title, page_content);
println!(
"Dry run: Would create wiki page '{}' with content {}",
page_title, page_content
);
return Ok("Dry run - no actual page created".to_string());
}
@ -230,7 +243,22 @@ impl Mediawiki {
}
let url = format!("{}/api.php", self.server_url);
let params: Box<[(&str, &str)]> = Box::from([
let params: Box<[(&str, &str)]> = match update_main_page {
ValidPageEdits::WithPotentiallyOverriding => {
// This means we *EDIT* the *Main Page* and do not prevent overwriting
Box::from([
("action", "edit"),
("format", "json"),
("title", page_title),
("text", page_content),
("token", self.csrf_token.get().unwrap()),
("bot", "true")
])
}
ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding => {
// This means we *CREATE* a *new Page* and always prevent overwriting
Box::from([
("action", "edit"),
("format", "json"),
("title", page_title),
@ -238,34 +266,61 @@ impl Mediawiki {
("token", self.csrf_token.get().unwrap()),
("createonly", "true"), // Prevent overwriting existing pages
("bot", "true"),
]);
])
}
ValidPageEdits::WithoutOverriding => {
// This means we *CREATE* a *new Page* and always prevent overwriting
Box::from([
("action", "edit"),
("format", "json"),
("title", page_title),
("text", page_content),
("token", self.csrf_token.get().unwrap()),
("createonly", "true"), // Prevent overwriting existing pages
("bot", "true"),
])
}
};
verboseln!("Current page title: {page_title}");
// Log the request details for debugging
verboseln!("Creating wiki page: {} at {}", page_title, url);
// Make the request to create the page
let request_result = self.make_request(url, params, ValidRequestTypes::PostForEditing)?;
let request_result: String =
self.make_request(url, params, ValidRequestTypes::PostForEditing)?;
verboseln!("pos1");
// Parse the response to check for success
let response: serde_json::Value = serde_json::from_str(&request_result)?;
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()
});
verboseln!("pos2");
// Check if the page creation was successful
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);
verboseln!("pos4");
} else {
print!("Failed to create wiki page. Response: {response}");
verboseln!("pos5");
}
} else {
print!("Unexpected response when creating wiki page: {response}");
verboseln!("pos6");
};
verboseln!("pos7");
// Update the main plenum page if requested
if update_main_page {
self.update_plenum_page(page_title)?;
}
Ok(request_result)
} else {
Err(format!("Failed to create wiki page. Response: {}", response).into())
}
} else {
Err(format!("Unexpected response when creating wiki page: {}", response).into())
}
match update_main_page {
ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding => {
verboseln!("updating main page...");
self.update_plenum_page(page_title)?
},
_ => ()
};
return Ok(page_title.to_string())
}
/// This function is responsible for updating the main plenum page:
@ -273,13 +328,18 @@ impl Mediawiki {
/// It downloads the main plenum page from Mediawiki, inserts the
/// new Link to the newly uploaded plenum pad and uploads the
/// page back to mediawiki.
pub fn update_plenum_page(&self, new_page_title_to_link_to: &str) -> Result<(), Box<dyn Error>> {
pub fn update_plenum_page(
&self, new_page_title_to_link_to: &str,
) -> Result<(), Box<dyn Error>> {
let current_year = Utc::now().year().to_string();
let year_heading_pattern = format!("=== {} ===", current_year);
// Download Plenum page content
let mut page_content = self.get_page_content(&self.plenum_main_page_name)?;
// 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
@ -291,24 +351,31 @@ impl Mediawiki {
let parts: Vec<&str> = page_content.split(&last_year_heading_pattern).collect();
page_content = format!(
"{}=== {} ===\n* [[{}]]\n\n{}{}",
parts[0], current_year, new_page_title_to_link_to,
parts[0],
current_year,
new_page_title_to_link_to,
last_year_heading_pattern,
parts[1]
);
} else {
// Fallback: add the current year heading to the end of the page
page_content.push_str(&format!("\n\n=== {} ===\n* [[{}]]", current_year, new_page_title_to_link_to));
page_content.push_str(&format!(
"\n\n=== {} ===\n* [[{}]]",
current_year, new_page_title_to_link_to
));
}
} else {
// Paste the link below the current year heading
page_content = page_content.replace(
&year_heading_pattern,
&format!("{}\n* [[{}]]", year_heading_pattern, new_page_title_to_link_to)
&format!("{}\n* [[{}]]", year_heading_pattern, new_page_title_to_link_to),
);
}
} else {
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, false)?;
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`)
@ -335,10 +402,8 @@ impl Mediawiki {
pub fn test_wiki_write(&self) -> Result<(), Box<dyn Error>> {
// Generate a unique test page title
let test_page_title = format!(
"TestPage/WikiWriteTest-{}",
chrono::Utc::now().format("%Y%m%d%H%M%S")
);
let test_page_title =
format!("TestPage/WikiWriteTest-{}", chrono::Utc::now().format("%Y%m%d%H%M%S"));
// Test content to write
let test_content = format!(
@ -369,7 +434,8 @@ impl Mediawiki {
}
// Make request and capture the full response
let request_result = match self.client
let request_result = match self
.client
.post(&url)
.basic_auth(&self.http_user, Some(&self.http_password))
.form(&params)
@ -386,12 +452,12 @@ impl Mediawiki {
},
Err(e) => {
return Err(format!("Failed to read response body: {}", e).into());
}
},
}
},
Err(e) => {
return Err(format!("Request failed: {}", e).into());
}
},
};
// Attempt to parse the response
@ -409,10 +475,11 @@ impl Mediawiki {
} else {
Err(format!("Unexpected response: {}", response).into())
}
}
},
Err(e) => {
Err(format!("JSON parsing failed. Error: {}. Raw response: {}", e, request_result).into())
}
Err(format!("JSON parsing failed. Error: {}. Raw response: {}", e, request_result)
.into())
},
}
}
}
@ -421,10 +488,12 @@ impl Mediawiki {
/// logging in to mediawiki, retrieving the necessary tokens, creating a
/// new wiki page for the current plenum protocol, and for linking the new
/// page to the main plenum overview page.
pub fn pad_ins_wiki(old_pad_content: String, wiki: &Mediawiki, plenum_date: &NaiveDate) -> Result<(), Box<dyn Error>> {
pub fn pad_ins_wiki(
old_pad_content: String, wiki: &Mediawiki, plenum_date: &NaiveDate,
) -> Result<(), Box<dyn Error>> {
// Use the provided date or default to current date
let date = plenum_date;
wiki.test_wiki_write()?;
// wiki.test_wiki_write()?;
// Login to Wiki and get required tokens for logging in and writing
verboseln!("logging in...");
let login_result = wiki.login()?;
@ -443,7 +512,7 @@ pub fn pad_ins_wiki(old_pad_content: String, wiki: &Mediawiki, plenum_date: &Nai
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, true)?;
wiki.new_wiki_page(&full_page_title, &pad_converted, ValidPageEdits::ModifyPlenumPageAfterwards_WithoutOverriding)?;
verboseln!("Finished successfully with wiki");
Ok(())