mediawiki progress

- mediawiki.rs:
 - new make_request function for easier post- and get-requests
 - new new_wiki_page function that creates new wiki pages
 - working csrf-Token-function
 - fixed read_txt_file function: Now properly working
 - pad_ins_wiki-function is now the main trigger for all the actions that should be done on day after plenum
- hedgedoc.rs + mediawiki.rs: Better error handling for common offline edge case
- main: ansi art moved in function, Timer and eta for time prediction during development
- still to do in mediawiki.rs: Date logic for pad generation and function for modifying the main Plenum Page
This commit is contained in:
murmeldin 2024-08-12 22:23:21 +02:00 committed by murmeldin
parent f12f6d05b1
commit e4444b8118
4 changed files with 174 additions and 64 deletions

View file

@ -22,6 +22,7 @@ serde = {version = "1.0.204", features = ["derive"]}
serde_json = "1.0.122" serde_json = "1.0.122"
colored = "2.1.0" colored = "2.1.0"
nom = "7.1.3" nom = "7.1.3"
mediawiki = "0.3.1"
[[bin]] [[bin]]
name = "Plenum-Bot" name = "Plenum-Bot"

View file

@ -54,9 +54,25 @@ impl HedgeDoc {
} }
fn do_request(&self, url: &str) -> Result<Response, Box<dyn Error>> { fn do_request(&self, url: &str) -> Result<Response, Box<dyn Error>> {
Ok(self.client.get(url).send().unwrap()) match self.client.get(url).send() {
Ok(response) => {
if response.status().is_success() {
Ok(response)
} else {
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())
}
}
}
} }
fn get_id_from_response(&self, res: Response) -> String { fn get_id_from_response(&self, res: Response) -> String {
res.url().to_string().trim_start_matches(&format!("{}/", self.server_url)).to_string() res.url().to_string().trim_start_matches(&format!("{}/", self.server_url)).to_string()
} }

View file

@ -22,25 +22,25 @@ Pad-ins-Wiki-und-versenden-Skript
main.rs main.rs
- [ ] Add logic for top_anzahl in main.rs:172 - [ ] Add logic for top_anzahl in main.rs:172
MediaWiki MediaWiki
- [ ] Add "get_csrf_token-function" for getting a write token to allow write operations in the wiki (murmeldin) - [X] Add "get_csrf_token-function" for getting a write token to allow write operations in the wiki (murmeldin)
- [ ] Add "create_page" function for creating new pages in the wiki that is called on every day after plenum (murmeldin) - [X] Add "create_page" function for creating new pages in the wiki that is called on every day after plenum (murmeldin)
- [ ] Add "modify_plenum_main_page" function for creating new Links on the Plenum main page whenever the create_page function is being called (murmeldin) - [ ] Add "modify_plenum_main_page" function for creating new Links on the Plenum main page whenever the create_page function is being called (murmeldin)
- [ ] Date Logic for create_page-function (maybe nobody or murmeldin when date_logic branch is done and merged)
future improvements: future improvements:
- search ADJ_TIMEYWIMEY to find places that need adjusting if the bot might run late - search ADJ_TIMEYWIMEY to find places that need adjusting if the bot might run late
(that's an incomplete list, but tag things as you notice them) (that's an incomplete list, but tag things as you notice them)
*/ */
use std::borrow::Cow;
use std::env; use std::env;
use std::error::Error; use std::error::Error;
use std::time::Instant;
use chrono::{Local, NaiveDate}; use chrono::{Local, NaiveDate};
use clap::{Arg, Command}; use clap::{Arg, Command};
use colored::Colorize; use colored::Colorize;
use regex::Regex; use regex::Regex;
use std::env;
use std::error::Error;
use std::fmt::Display; use std::fmt::Display;
use std::io::IsTerminal; use std::io::IsTerminal;
@ -160,6 +160,7 @@ fn parse_args() -> Args {
/* ***** Main ***** */ /* ***** Main ***** */
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let start = Instant::now();
if std::io::stdout().is_terminal() { if std::io::stdout().is_terminal() {
println!(include_str!("chaosknoten.txt"), VERSION = env!("CARGO_PKG_VERSION")); println!(include_str!("chaosknoten.txt"), VERSION = env!("CARGO_PKG_VERSION"));
} }
@ -275,6 +276,11 @@ fn main() -> Result<(), Box<dyn Error>> {
// shutdown // shutdown
config.set("state-last-run", &today.to_string())?; config.set("state-last-run", &today.to_string())?;
let duration = format!("{:?}", start.elapsed());
config.set("state-eta", &duration).ok();
verboseln!("Der Bot ist fertig und hat für die Ausführung {} gebraucht.", duration);
if config.has_errors() { if config.has_errors() {
Err("There were errors while writing config values.".into()) Err("There were errors while writing config values.".into())
} else { } else {
@ -679,3 +685,12 @@ fn send_email(
); );
email.send_email(full_subject, full_body) email.send_email(full_subject, full_body)
} }
fn display_logo(eta: &str) {
let ansi_art = r#"
Version 1.1,"#;
let ansi_art = format!("{ansi_art} ETA = {eta}\n");
println!("{}", ansi_art.red());
}

View file

@ -36,6 +36,11 @@ pub const CONFIG: CfgGroup<'static> = CfgGroup {
key: "api-secret", key: "api-secret",
description: "API secret / \"password\" used for authenticating as the bot.", description: "API secret / \"password\" used for authenticating as the bot.",
}, },
CfgField::Default {
key: "eta",
default: "no ETA, program never ran",
description: "ETA message for estimating time the program takes"
}
], ],
}; };
@ -63,6 +68,12 @@ impl std::fmt::Debug for Mediawiki {
} }
} }
pub enum RequestType {
Get,
Post,
PostForEditing
}
impl Mediawiki { impl Mediawiki {
pub fn new( pub fn new(
server_url: &str, http_auth_user: &str, http_auth_password: &str, api_user: &str, api_secret: &str, is_dry_run: bool, server_url: &str, http_auth_user: &str, http_auth_password: &str, api_user: &str, api_secret: &str, is_dry_run: bool,
@ -81,84 +92,155 @@ impl Mediawiki {
} }
pub fn get_login_token(&self) -> Result<String, Box<dyn Error>> { pub fn get_login_token(&self) -> Result<String, Box<dyn Error>> {
let url = let url =
format!("{}/api.php?action=query&meta=tokens&type=login&format=json", self.server_url); format!("{}/api.php?", self.server_url);
let resp = self let params: Box<[(&str, &str)]> = Box::from( [
.client ("format", "json"),
.get(url) ("meta", "tokens"),
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT ("type", "login"),
.send()? ("action", "query")
.text()?; ]);
let response_deserialized: QueryResponse = serde_json::from_str(&resp)?; let resp = self.make_request(url, params, RequestType::Get).unwrap();
let response_deserialized: QueryResponseLogin = serde_json::from_str(&resp)?;
Ok(response_deserialized.query.tokens.logintoken) Ok(response_deserialized.query.tokens.logintoken)
} }
pub fn login (&self) -> Result<String, Box<dyn Error>> { pub fn login (&self) -> Result<String, Box<dyn Error>> {
let url = format!("{}/api.php?action=login", self.server_url); let url = format!("{}/api.php?", self.server_url);
let params = [ let params: Box<[(&str, &str)]> = Box::from([
("lgname", &self.api_user), ("lgname", self.api_user.as_str()),
("lgpassword", &self.api_secret), ("lgpassword", self.api_secret.as_str()),
("lgtoken", &self.login_token) ("lgtoken", &self.login_token),
]; ("action", "login")
let resp = self ]);
.client let resp: Result<String, Box<dyn Error>> = self.make_request(url, params, RequestType::Post);
.post(url) Ok(resp.unwrap())
.form(&params)
.send()?
.text()?;
Ok(resp)
} }
pub fn get_csrf_token(&self) -> Result<String, Box<dyn Error>> { // HAS TO BE FIXED pub fn get_csrf_token(&self) -> Result<String, Box<dyn Error>> { // HAS TO BE FIXED
// action=query&meta=tokens
let url = let url =
format!("{}/api.php?", self.server_url); format!("{}/api.php?", self.server_url);
let params = [ let params: Box<[(&str, &str)]> = Box::from([
("format", "json"), ("format", "json"),
("meta", "tokens"), ("meta", "tokens"),
("formatversion", "2"), ("formatversion", "2"),
("lgtoken", &self.login_token) ("action", "query")
]; ]);
let resp = self let resp: Result<String, Box<dyn Error>> = self.make_request(url, params, RequestType::Get);
.client let resp = resp.unwrap();
.get(url) let response_deserialized: QueryResponseCsrf = serde_json::from_str(&resp)?;
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT Ok(response_deserialized.query.tokens.csrftoken)
.query(&params)
.send()?
.text()?;
println!(" --- \n{}\n ---", resp.green());
let response_deserialized: QueryResponse = serde_json::from_str(&resp)?;
Ok(response_deserialized.query.tokens.logintoken)
} }
pub fn make_request(&self, url: String, params: Box<[(&str, &str)]>, request_type: RequestType) -> Result<String, Box<dyn Error>> {
let resp: Result<String, Box<dyn Error>> = match
match request_type {
RequestType::Get => {
self
.client
.get(url)
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT
.query(&params)
.send()
}
RequestType::Post | RequestType::PostForEditing => {
self
.client
.post(url)
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT
.form(&params)
.send()
}
}
{
Ok(response) => {
if response.status().is_success() {
match request_type {
RequestType::PostForEditing => Ok(response.text().unwrap()),
_ => Ok(response.text().unwrap())
}
}
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())
}
}
};
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) -> Result<String, Box<dyn Error>> {
// action=edit&format=json&title=Wikipedia:Sandbox&appendtext=Hello&token=sampleCsrfToken123+\
let url =
format!("{}/api.php?", self.server_url);
let params: Box<[(&str, &str)]> = Box::from([
("action", "edit"), // Create and edit pages.
("format", "json"),
("title", page_title), // Title of the page to edit. Cannot be used together with pageid.
("appendtext", page_content), // Add this text to the end of the page or section. Overrides text.
("token", self.csrf_token.as_str()), // A "csrf" token retrieved from action=query&meta=tokens
("bot", "true"), // Mark this edit as a bot edit.
]);
self.make_request(url, params, RequestType::Post)
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct QueryResponse { struct QueryResponseLogin {
batchcomplete: String, batchcomplete: String,
query: QueryTokens, query: QueryTokensLogin,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct QueryTokens { struct QueryTokensLogin {
tokens: Tokens, tokens: TokensLogin,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Tokens { struct TokensLogin {
logintoken: String, logintoken: String,
} }
pub fn pad_ins_wiki(old_pad_content: String, wiki: &mut Mediawiki) -> Result<(), Box<dyn Error>> { #[derive(Deserialize)]
convert_md_to_mediawiki(old_pad_content); struct QueryResponseCsrf {
batchcomplete: bool,
query: crate::mediawiki::QueryTokensCsrf,
}
let auth_result = wiki.get_login_token().unwrap(); #[derive(Deserialize)]
wiki.login_token = auth_result.clone(); struct QueryTokensCsrf {
tokens: crate::mediawiki::TokensCsrf,
}
#[derive(Deserialize)]
struct TokensCsrf {
csrftoken: String,
}
pub fn pad_ins_wiki(old_pad_content: String, wiki: &mut Mediawiki) -> Result<(), Box<dyn Error>> {
// Login to Wiki and get required tokens for logging in and writing
let auth_result = wiki.get_login_token()?;
wiki.login_token.clone_from(&auth_result);
println!("AUTH Success"); println!("AUTH Success");
let login_result = wiki.login()?; let login_result = wiki.login()?;
println!("LOGIN Success"); println!("LOGIN Success");
let csrf_token = wiki.get_csrf_token(); let csrf_token = wiki.get_csrf_token();
let csrf_token = csrf_token.unwrap_or_else(|e| {
println!("Error while trying to get csrf: {:?}", e);
String::new()
});
println!("CSRF Success"); println!("CSRF Success");
//wiki.csrf_token.clone_from(&csrf_token); HAS TO BE FIXED wiki.csrf_token.clone_from(&csrf_token);
println!("---AUTH RESULT:---\n{}\n---LOGIN RESULT:---\n{:?}\n---CSRF RESULT:---\n\n-----------", auth_result, login_result,/* csrf_token*/); println!("---AUTH RESULT:---\n{}\n---LOGIN RESULT:---\n{:?}\n---CSRF RESULT:---\n{}\n-----------", auth_result, login_result, csrf_token);
// Convert to mediawiki and make new page
let pad_converted = convert_md_to_mediawiki(old_pad_content);
println!("Das kommt ins Wiki: {}", pad_converted);
//wiki.new_wiki_page("Page Test 5", &pad_converted)?; JUST AN EXAMPLE
// Textdatei wieder einlesen // Textdatei wieder einlesen
// Passwörter aus Datenbank lesen (ToBeDone) // Passwörter aus Datenbank lesen (ToBeDone)
@ -192,11 +274,11 @@ fn pandoc_convert(
/// Reads a text file from a specified path and returns it as a String /// Reads a text file from a specified path and returns it as a String
fn read_txt_file(filepath: &str) -> String { fn read_txt_file(filepath: &str) -> String {
let mut file = File::open(filepath) let mut file = File::open(filepath)
.expect(&*format!("Fehler beim öffnen der Textdatei mit Pfad {filepath}!")); .unwrap_or_else(|_| panic!("Fehler beim öffnen der Textdatei mit Pfad {filepath}!"));
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents) file.read_to_string(&mut contents)
.expect("Fehler beim auslesen der MediaWiki-Textdatei!") .expect("Fehler beim auslesen der MediaWiki-Textdatei!");
.to_string() contents
} }
/// Takes a Sting in the Markdown format and returns a String in the mediawiki Format /// Takes a Sting in the Markdown format and returns a String in the mediawiki Format
@ -204,12 +286,8 @@ fn convert_md_to_mediawiki(old_pad_content: String) -> String {
// TODO: use tempfile="3.3", make it a NamedTempFile::new()?; // TODO: use tempfile="3.3", make it a NamedTempFile::new()?;
// or alternatively use piped stdout to avoid files entirely // or alternatively use piped stdout to avoid files entirely
let output_filepath: &str = "./pandoc_mediawiki.txt"; let output_filepath: &str = "./pandoc_mediawiki.txt";
pandoc_convert( pandoc_convert(old_pad_content, output_filepath, pandoc::InputFormat::Markdown, pandoc::OutputFormat::MediaWiki).expect("Fehler beim Umwandeln des und speichern des Pads in eine mediawiki-Textdatei");
old_pad_content, let temp = read_txt_file(output_filepath);
output_filepath, println!("TEMP: {}", temp.purple());
pandoc::InputFormat::Markdown, temp
pandoc::OutputFormat::MediaWiki,
)
.expect("Fehler beim Umwandeln des und speichern des Pads in eine mediawiki-Textdatei");
read_txt_file(output_filepath)
} }