Compare commits
10 commits
9afc16ca0c
...
9583d9c37d
Author | SHA1 | Date | |
---|---|---|---|
9583d9c37d | |||
20b8ce40de | |||
659e9f258a | |||
bbd9b2c29e | |||
aca180d6ba | |||
b04d35ee6b | |||
d3681e1699 | |||
3608838949 | |||
12450ce5a3 | |||
2da4a149c9 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,9 +1,8 @@
|
|||
/target
|
||||
/.idea
|
||||
/src/debug_emails.txt
|
||||
Cargo.lock
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
.direnv
|
||||
shell.nix
|
||||
pandoc*.txt
|
||||
.envrc
|
||||
|
|
2896
Cargo.lock
generated
Normal file
2896
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
stdext = "0.3.3"
|
||||
pandoc = "0.8"
|
||||
chrono = "0.4.38"
|
||||
regex = "1.10.5"
|
||||
futures = "0.3.30"
|
||||
|
|
3
architecture.md
Normal file
3
architecture.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Architecture
|
||||
|
||||
This document is intended to explain how the Plenum Bot works.
|
25
shell.nix
Normal file
25
shell.nix
Normal file
|
@ -0,0 +1,25 @@
|
|||
{pkgs ? import <nixpkgs> {}}: let
|
||||
rust-toolchain = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = with pkgs; [rustc cargo rustPlatform.rustcSrc rustfmt clippy];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs.buildPackages; [
|
||||
rust-toolchain
|
||||
|
||||
pkg-config
|
||||
xe
|
||||
xz
|
||||
|
||||
cargo-tarpaulin
|
||||
openssl
|
||||
sqlite
|
||||
gcc
|
||||
gnumake
|
||||
|
||||
# dotnet-sdk_8
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
}
|
27
src/lib.rs
27
src/lib.rs
|
@ -7,6 +7,33 @@ pub mod mediawiki;
|
|||
pub mod template;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::io::{Read, Write};
|
||||
use std::process::{Command, ExitStatus, Stdio};
|
||||
|
||||
fn pipe(
|
||||
program: &str, args: &mut [&str], input: String,
|
||||
) -> Result<(String, String, ExitStatus), Box<dyn Error>> {
|
||||
let mut cmd = Command::new(program)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
if let Some(mut stdin) = cmd.stdin.take() {
|
||||
stdin.write_all(input.as_bytes())?;
|
||||
}
|
||||
let mut output = String::new();
|
||||
if let Some(mut stdout) = cmd.stdout.take() {
|
||||
stdout.read_to_string(&mut output)?;
|
||||
}
|
||||
let mut errmsg = String::new();
|
||||
if let Some(mut stderr) = cmd.stderr.take() {
|
||||
stderr.read_to_string(&mut errmsg)?;
|
||||
}
|
||||
let status = cmd.wait()?;
|
||||
Ok((output, errmsg, status))
|
||||
}
|
||||
|
||||
/// Checks environment variable `DRY_RUN` to see if any external operations
|
||||
/// should *actually* be done.
|
||||
|
|
30
src/main.rs
30
src/main.rs
|
@ -2,25 +2,23 @@
|
|||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::io::IsTerminal;
|
||||
use std::os::linux::raw::stat;
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::{Local, NaiveDate};
|
||||
use chrono::{Local, NaiveDate, Utc, DateTime};
|
||||
use clap::{Arg, Command};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use cccron_lib::{trace_var, trace_var_, verboseln};
|
||||
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
|
||||
use cccron_lib::key_value::KeyValueStore as KV;
|
||||
|
||||
use cccron_lib::date;
|
||||
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::mediawiki::{self, Mediawiki};
|
||||
use cccron_lib::{verboseln,trace_var,trace_var_};
|
||||
use cccron_lib::NYI;
|
||||
|
||||
/* ***** Config Spec ***** */
|
||||
|
@ -93,6 +91,16 @@ fn today() -> NaiveDate {
|
|||
.map(|v| NaiveDate::parse_from_str(&v, "%F").expect("'TODAY' hat nicht format YYYY-MM-DD"))
|
||||
.unwrap_or(Local::now().date_naive())
|
||||
}
|
||||
/// Gets either the state from the config or overrides it with the state from
|
||||
/// the environment variable `STATE_OVERRIDE` (for testing purposes.)
|
||||
///
|
||||
/// For example, `STATE_OVERRIDE=Waiting` can be used to debug mediawiki.
|
||||
fn state(config: &KeyValueStore) -> ProgramState {
|
||||
match env::var("STATE_OVERRIDE") {
|
||||
Ok(val) => ProgramState::parse(&val),
|
||||
Err(_e) => ProgramState::parse(&config["state-name"])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Args {
|
||||
|
@ -170,7 +178,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
&config["wiki-http-password"],
|
||||
&config["wiki-api-user"],
|
||||
&config["wiki-api-secret"],
|
||||
is_dry_run(),
|
||||
false, // is_dry_run(), // TODO: Remove this false in order for actually letting dry_run affecting if mediawiki is run
|
||||
&config["wiki-plenum-page"],
|
||||
);
|
||||
trace_var_!(wiki);
|
||||
// get next plenum days
|
||||
|
@ -181,7 +190,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
let nearest_plenum_days = date::get_matching_dates_around(today, plenum_spec);
|
||||
trace_var!(nearest_plenum_days);
|
||||
// figure out where we are
|
||||
let mut last_state = ProgramState::parse(&config["state-name"]);
|
||||
let mut last_state = state(&config);
|
||||
let last_run = config.get("state-last-run").unwrap_or_default();
|
||||
let last_run = NaiveDate::parse_from_str(&last_run, "%Y-%m-%d").unwrap_or_default();
|
||||
trace_var!(last_run);
|
||||
|
@ -475,6 +484,7 @@ fn do_protocol(
|
|||
);
|
||||
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 {
|
||||
|
|
464
src/mediawiki.rs
464
src/mediawiki.rs
|
@ -1,12 +1,13 @@
|
|||
use std::cell::OnceCell;
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use chrono::{Datelike, NaiveDate, Utc};
|
||||
use colored::Colorize;
|
||||
use pandoc::{PandocError, PandocOutput};
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{trace_var, verboseln};
|
||||
use crate::config_spec::{CfgField, CfgGroup};
|
||||
|
||||
pub const CONFIG: CfgGroup<'static> = CfgGroup {
|
||||
|
@ -36,10 +37,15 @@ pub const CONFIG: CfgGroup<'static> = CfgGroup {
|
|||
key: "api-secret",
|
||||
description: "API secret / \"password\" used for authenticating as the bot.",
|
||||
},
|
||||
CfgField::Default {
|
||||
key: "plenum-page",
|
||||
default: "Plenum",
|
||||
description: "The name of the wiki page where all new plenum pages will be linked.",
|
||||
},
|
||||
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."
|
||||
}
|
||||
],
|
||||
};
|
||||
|
@ -51,8 +57,9 @@ pub struct Mediawiki {
|
|||
api_user: String,
|
||||
api_secret: String,
|
||||
is_dry_run: bool,
|
||||
login_token: String,
|
||||
csrf_token: String,
|
||||
login_token: OnceCell<String>,
|
||||
csrf_token: OnceCell<String>,
|
||||
plenum_main_page_name: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
|
@ -68,7 +75,7 @@ impl std::fmt::Debug for Mediawiki {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum RequestType {
|
||||
pub enum ValidRequestTypes {
|
||||
Get,
|
||||
Post,
|
||||
PostForEditing
|
||||
|
@ -76,7 +83,7 @@ pub enum RequestType {
|
|||
|
||||
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,
|
||||
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(),
|
||||
|
@ -85,67 +92,104 @@ impl Mediawiki {
|
|||
api_user: api_user.to_string(),
|
||||
api_secret: api_secret.to_string(),
|
||||
is_dry_run,
|
||||
login_token: String::new(),
|
||||
csrf_token: String::new(),
|
||||
login_token: OnceCell::new(),
|
||||
csrf_token: OnceCell::new(),
|
||||
plenum_main_page_name: plenum_main_page_name.to_string(),
|
||||
client: Client::builder().cookie_store(true).build().unwrap(),
|
||||
}
|
||||
}
|
||||
pub fn get_login_token(&self) -> Result<String, Box<dyn Error>> {
|
||||
let url =
|
||||
format!("{}/api.php?", self.server_url);
|
||||
let params: Box<[(&str, &str)]> = Box::from( [
|
||||
("format", "json"),
|
||||
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"),
|
||||
("action", "query")
|
||||
("format", "json")
|
||||
]);
|
||||
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)
|
||||
}
|
||||
pub fn login (&self) -> Result<String, Box<dyn Error>> {
|
||||
let url = format!("{}/api.php?", self.server_url);
|
||||
let params: Box<[(&str, &str)]> = Box::from([
|
||||
("lgname", self.api_user.as_str()),
|
||||
("lgpassword", self.api_secret.as_str()),
|
||||
("lgtoken", &self.login_token),
|
||||
("action", "login")
|
||||
verboseln!("Login params: {:?}", params_0);
|
||||
let resp_0: String = self.make_request(url.clone(), params_0, ValidRequestTypes::Get)?;
|
||||
verboseln!("Raw response login_0: {}", resp_0.yellow());
|
||||
let resp_0_deserialized: serde_json::Value = serde_json::from_str(&resp_0)?;
|
||||
verboseln!("login0 deserialized");
|
||||
|
||||
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!("login0 finished");
|
||||
let login_token: String = self.login_token.clone().into_inner().unwrap();
|
||||
verboseln!("using {login_token} as login");
|
||||
let params_1: Box<[(&str, &str)]> = Box::from([
|
||||
("action", "login"),
|
||||
("lgname", &self.api_user),
|
||||
("lgpassword", &self.api_secret),
|
||||
("lgtoken", &login_token),
|
||||
("format", "json")
|
||||
]);
|
||||
let resp: Result<String, Box<dyn Error>> = self.make_request(url, params, RequestType::Post);
|
||||
Ok(resp.unwrap())
|
||||
verboseln!("Login params: {:?}", params_1);
|
||||
let resp_1: String = self.client
|
||||
.post(&url)
|
||||
// .basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
|
||||
.form(¶ms_1)
|
||||
.send()?
|
||||
.text()?;
|
||||
verboseln!("Raw response login_1: {}", resp_1.yellow());
|
||||
let resp_1_deserialized: serde_json::Value = serde_json::from_str(&resp_1)?;
|
||||
if let Some(result) = resp_1_deserialized.get("login").and_then(|l| l.get("result")) {
|
||||
if result.as_str() == Some("Success") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Login failed. Response: {}", resp_1_deserialized).into())
|
||||
}
|
||||
pub fn get_csrf_token(&self) -> Result<String, Box<dyn Error>> { // HAS TO BE FIXED
|
||||
} else {
|
||||
Err(format!("Unexpected login response: {}", resp_1_deserialized).into())
|
||||
}
|
||||
}
|
||||
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"),
|
||||
("formatversion", "2"),
|
||||
("action", "query")
|
||||
]);
|
||||
let resp: Result<String, Box<dyn Error>> = self.make_request(url, params, RequestType::Get);
|
||||
let resp = resp.unwrap();
|
||||
let resp: String = self.make_request(url, params, ValidRequestTypes::Get)?;
|
||||
verboseln!("Raw response csrf: {}", resp);
|
||||
let response_deserialized: QueryResponseCsrf = serde_json::from_str(&resp)?;
|
||||
Ok(response_deserialized.query.tokens.csrftoken)
|
||||
let token = response_deserialized.query.tokens.csrftoken;
|
||||
verboseln!("Parsed token: '{}'", token);
|
||||
if token == "+\\" {
|
||||
return Err("Failed to parse CSRF token. Response was only '+\\'. Please check your Bot API credentials".into());
|
||||
}
|
||||
self.csrf_token.set(token)?;
|
||||
verboseln!("CSRF token acquired: {}", self.csrf_token.get().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn make_request(&self, url: String, params: Box<[(&str, &str)]>, request_type: RequestType) -> Result<String, Box<dyn Error>> {
|
||||
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 {
|
||||
RequestType::Get => {
|
||||
ValidRequestTypes::Get => {
|
||||
self
|
||||
.client
|
||||
.get(url)
|
||||
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT
|
||||
//.basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
|
||||
.query(¶ms)
|
||||
.send()
|
||||
}
|
||||
RequestType::Post | RequestType::PostForEditing => {
|
||||
ValidRequestTypes::Post | ValidRequestTypes::PostForEditing => {
|
||||
// convert the params into a HashMap for JSON
|
||||
let params_map: std::collections::HashMap<_, _> = params.iter().cloned().collect();
|
||||
self
|
||||
.client
|
||||
.post(url)
|
||||
//.basic_auth(&self.http_user, Some(&self.http_password)) ZU TESTZWECKEN ENTFERNT
|
||||
.form(¶ms)
|
||||
//.basic_auth(&self.http_user, Some(&self.http_password)) // TODO: ZU TESTZWECKEN ENTFERNT
|
||||
.query(¶ms_map)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
|
@ -153,8 +197,8 @@ impl Mediawiki {
|
|||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
match request_type {
|
||||
RequestType::PostForEditing => Ok(response.text().unwrap()),
|
||||
_ => Ok(response.text().unwrap())
|
||||
ValidRequestTypes::PostForEditing => Ok(response.text()?),
|
||||
_ => Ok(response.text()?)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -171,25 +215,267 @@ 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) -> Result<String, Box<dyn Error>> {
|
||||
// action=edit&format=json&title=Wikipedia:Sandbox&appendtext=Hello&token=sampleCsrfToken123+\
|
||||
let url =
|
||||
format!("{}/api.php?", self.server_url);
|
||||
pub fn new_wiki_page(&self, page_title: &str, page_content: &str, update_main_page: bool) -> 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);
|
||||
return Ok("Dry run - no actual page created".to_string());
|
||||
}
|
||||
|
||||
// Ensure we have a CSRF token
|
||||
if self.csrf_token.get().is_none() {
|
||||
return Err("CSRF token not set. Call get_csrf_token() first.".into());
|
||||
}
|
||||
|
||||
let url = format!("{}/api.php", self.server_url);
|
||||
let params: Box<[(&str, &str)]> = Box::from([
|
||||
("action", "edit"), // Create and edit pages.
|
||||
("action", "edit"),
|
||||
("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.
|
||||
("title", page_title),
|
||||
("text", page_content),
|
||||
("token", self.csrf_token.get().unwrap()),
|
||||
("createonly", "true"), // Prevent overwriting existing pages
|
||||
("bot", "true"),
|
||||
]);
|
||||
self.make_request(url, params, RequestType::Post)
|
||||
|
||||
// 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)?;
|
||||
|
||||
// Parse the response to check for success
|
||||
let response: serde_json::Value = serde_json::from_str(&request_result)?;
|
||||
|
||||
// Check if the page creation was successful
|
||||
if let Some(edit) = response.get("edit") {
|
||||
if edit.get("result").and_then(|r| r.as_str()) == Some("Success") {
|
||||
verboseln!("Successfully created wiki page: {}", page_title);
|
||||
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is responsible for updating the main plenum page:
|
||||
///
|
||||
/// 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>> {
|
||||
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 if the current year heading pattern exists
|
||||
if !page_content.contains(&year_heading_pattern) {
|
||||
// If not, add a new year heading pattern
|
||||
let last_year = (current_year.parse::<i32>()? - 1).to_string();
|
||||
let last_year_heading_pattern = format!("=== {} ===", last_year);
|
||||
|
||||
if page_content.contains(&last_year_heading_pattern) {
|
||||
// add a new year heading pattern before the last year one
|
||||
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,
|
||||
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));
|
||||
}
|
||||
} 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)
|
||||
);
|
||||
}
|
||||
|
||||
// refresh page
|
||||
self.new_wiki_page(&self.plenum_main_page_name, &page_content, false)?;
|
||||
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`)
|
||||
pub fn get_page_content(&self, page_title: &str) -> Result<String, Box<dyn Error>> {
|
||||
let url = format!("{}/api.php?", self.server_url);
|
||||
let params: Box<[(&str, &str)]> = Box::from([
|
||||
("action", "parse"),
|
||||
("prop", "wikitext"),
|
||||
("format", "json"),
|
||||
("page", page_title),
|
||||
("formatversion", "2"),
|
||||
]);
|
||||
let resp = self.make_request(url, params, ValidRequestTypes::Get)?;
|
||||
let response_deserialized: serde_json::Value = serde_json::from_str(&resp)?;
|
||||
|
||||
let wikitext = response_deserialized
|
||||
.get("parse")
|
||||
.and_then(|parse| parse.get("wikitext"))
|
||||
.and_then(|text| text.as_str())
|
||||
.ok_or("Expected field `wikitext` not found")?;
|
||||
|
||||
Ok(wikitext.to_string())
|
||||
}
|
||||
|
||||
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")
|
||||
);
|
||||
|
||||
// Test content to write
|
||||
let test_content = format!(
|
||||
"Wiki Write Test\n\nThis is a test page generated at {}. \nIt can be safely deleted.",
|
||||
chrono::Utc::now()
|
||||
);
|
||||
|
||||
// Ensure login token and CSRF token are set
|
||||
let _login_result = self.login()?;
|
||||
self.get_csrf_token()?;
|
||||
|
||||
let url = format!("{}/api.php", self.server_url);
|
||||
let params: Box<[(&str, &str)]> = Box::from([
|
||||
("action", "edit"),
|
||||
("format", "json"),
|
||||
("title", &test_page_title),
|
||||
("text", &test_content),
|
||||
("token", self.csrf_token.get().unwrap()),
|
||||
("createonly", "true"),
|
||||
("bot", "true"),
|
||||
]);
|
||||
|
||||
// Manually print out all parameters for debugging
|
||||
println!("Debug - URL: {}", url);
|
||||
println!("Debug - Parameters:");
|
||||
for (key, value) in params.iter() {
|
||||
println!(" {}: {}", key, value);
|
||||
}
|
||||
|
||||
// Make request and capture the full response
|
||||
let request_result = match self.client
|
||||
.post(&url)
|
||||
.basic_auth(&self.http_user, Some(&self.http_password))
|
||||
.form(¶ms)
|
||||
.send()
|
||||
{
|
||||
Ok(response) => {
|
||||
println!("Debug - Response Status: {}", response.status());
|
||||
println!("Debug - Response Headers: {:?}", response.headers());
|
||||
|
||||
match response.text() {
|
||||
Ok(text) => {
|
||||
println!("Debug - Raw Response Body:\n{}", text);
|
||||
text
|
||||
},
|
||||
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
|
||||
match serde_json::from_str::<serde_json::Value>(&request_result) {
|
||||
Ok(response) => {
|
||||
println!("Debug - Parsed Response: {}", response);
|
||||
|
||||
if let Some(edit) = response.get("edit") {
|
||||
if edit.get("result").and_then(|r| r.as_str()) == Some("Success") {
|
||||
println!("✅ Successfully created test page: {}", test_page_title);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Failed to create page. Response: {}", response).into())
|
||||
}
|
||||
} else {
|
||||
Err(format!("Unexpected response: {}", response).into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Err(format!("JSON parsing failed. Error: {}. Raw response: {}", e, request_result).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function runs at the end of do_protocol() and is responsible for
|
||||
/// 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>> {
|
||||
// Use the provided date or default to current date
|
||||
let date = plenum_date;
|
||||
wiki.test_wiki_write()?;
|
||||
// Login to Wiki and get required tokens for logging in and writing
|
||||
verboseln!("logging in...");
|
||||
let login_result = wiki.login()?;
|
||||
verboseln!("Login done.");
|
||||
trace_var!(login_result);
|
||||
|
||||
wiki.get_csrf_token()?;
|
||||
verboseln!("CSRF token acquired.");
|
||||
|
||||
// Convert to mediawiki and make new page
|
||||
let pad_converted = pandoc_convert(old_pad_content)?;
|
||||
trace_var!(pad_converted);
|
||||
|
||||
// Create a new wiki page plenum_main_page_name/page_title, e.g. under Plenum/13._August_2024
|
||||
verboseln!("wiki: uploading converted pad");
|
||||
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)?;
|
||||
|
||||
verboseln!("Finished successfully with wiki");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes a String in the Markdown format, converts it and returns it in
|
||||
/// the Mediawiki format
|
||||
fn pandoc_convert(markdown: String) -> Result<String, Box<dyn Error>> {
|
||||
let (output, errors, status) = crate::pipe(
|
||||
"pandoc",
|
||||
&mut ["--from", "markdown-auto_identifiers", "--to", "mediawiki", "--no-highlight"],
|
||||
markdown,
|
||||
)?;
|
||||
if status.success() {
|
||||
println!("Resultat von Pandoc: {}", output);
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(format!("Pandoc error, exit code {:?}\n{}", status, errors).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_page_title(date: &NaiveDate) -> String {
|
||||
date.format("%d. %B %Y").to_string()
|
||||
}
|
||||
|
||||
/// Deserialization must be done this way because the response contains
|
||||
/// two `\\` characters in both the login and csrf tokens, which breaks the
|
||||
/// usual deserialization
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QueryResponseLogin {
|
||||
#[allow(dead_code)]
|
||||
batchcomplete: String,
|
||||
query: QueryTokensLogin,
|
||||
}
|
||||
|
@ -206,7 +492,7 @@ struct TokensLogin {
|
|||
|
||||
#[derive(Deserialize)]
|
||||
struct QueryResponseCsrf {
|
||||
batchcomplete: bool,
|
||||
#[allow(dead_code)]
|
||||
query: crate::mediawiki::QueryTokensCsrf,
|
||||
}
|
||||
|
||||
|
@ -219,75 +505,3 @@ struct QueryTokensCsrf {
|
|||
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");
|
||||
let login_result = wiki.login()?;
|
||||
println!("LOGIN Success");
|
||||
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");
|
||||
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);
|
||||
|
||||
// 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
|
||||
|
||||
// Passwörter aus Datenbank lesen (ToBeDone)
|
||||
/*
|
||||
let plenum_bot_user = String::from("PlenumBot@PlenumBot-PW1");
|
||||
let plenum_bot_pw = String::from("**OLD_API_PW_REMOVED**");
|
||||
let login_token = login_to_mediawiki(plenum_bot_user.clone(), plenum_bot_pw.clone())
|
||||
.expect("Fehler beim Einloggen!");
|
||||
println!("plenum_bot_user: {plenum_bot_user}, plenum_bot_pw: {plenum_bot_pw}, login_token: {login_token}")
|
||||
|
||||
*/
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts one file type into another using pandoc and saves the result as a txt file
|
||||
fn pandoc_convert(
|
||||
old_pad_content: String, output_filepath: &str, input_format: pandoc::InputFormat,
|
||||
output_format: pandoc::OutputFormat,
|
||||
) -> Result<PandocOutput, PandocError> {
|
||||
//Convert Markdown into Mediawiki
|
||||
// Vanilla pandoc Befehl: pandoc --from markdown --to mediawiki --no-highlight
|
||||
let mut p = pandoc::new();
|
||||
p.set_input(pandoc::InputKind::Pipe(old_pad_content));
|
||||
p.set_input_format(input_format, vec![]);
|
||||
p.set_output(pandoc::OutputKind::File(output_filepath.parse().unwrap()));
|
||||
p.set_output_format(output_format, vec![]);
|
||||
p.execute()
|
||||
}
|
||||
|
||||
|
||||
/// Reads a text file from a specified path and returns it as a String
|
||||
fn read_txt_file(filepath: &str) -> String {
|
||||
let mut file = File::open(filepath)
|
||||
.unwrap_or_else(|_| panic!("Fehler beim öffnen der Textdatei mit Pfad {filepath}!"));
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)
|
||||
.expect("Fehler beim auslesen der MediaWiki-Textdatei!");
|
||||
contents
|
||||
}
|
||||
|
||||
/// Takes a Sting in the Markdown format and returns a String in the mediawiki Format
|
||||
fn convert_md_to_mediawiki(old_pad_content: String) -> String {
|
||||
// TODO: use tempfile="3.3", make it a NamedTempFile::new()?;
|
||||
// or alternatively use piped stdout to avoid files entirely
|
||||
let output_filepath: &str = "./pandoc_mediawiki.txt";
|
||||
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");
|
||||
let temp = read_txt_file(output_filepath);
|
||||
println!("TEMP: {}", temp.purple());
|
||||
temp
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue