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
|
/target
|
||||||
/.idea
|
/.idea
|
||||||
/src/debug_emails.txt
|
/src/debug_emails.txt
|
||||||
Cargo.lock
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.direnv
|
.direnv
|
||||||
shell.nix
|
|
||||||
pandoc*.txt
|
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]
|
[dependencies]
|
||||||
stdext = "0.3.3"
|
stdext = "0.3.3"
|
||||||
pandoc = "0.8"
|
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
regex = "1.10.5"
|
regex = "1.10.5"
|
||||||
futures = "0.3.30"
|
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;
|
pub mod template;
|
||||||
|
|
||||||
use std::env;
|
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
|
/// Checks environment variable `DRY_RUN` to see if any external operations
|
||||||
/// should *actually* be done.
|
/// should *actually* be done.
|
||||||
|
|
30
src/main.rs
30
src/main.rs
|
@ -2,25 +2,23 @@
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::io::IsTerminal;
|
||||||
|
use std::os::linux::raw::stat;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use chrono::{Local, NaiveDate};
|
use chrono::{Local, NaiveDate, Utc, DateTime};
|
||||||
use clap::{Arg, Command};
|
use clap::{Arg, Command};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use cccron_lib::{trace_var, trace_var_, verboseln};
|
||||||
use std::fmt::Display;
|
|
||||||
use std::io::IsTerminal;
|
|
||||||
|
|
||||||
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
|
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
|
||||||
use cccron_lib::key_value::KeyValueStore as KV;
|
|
||||||
|
|
||||||
use cccron_lib::date;
|
use cccron_lib::date;
|
||||||
use cccron_lib::email::{self, Email, SimpleEmail};
|
use cccron_lib::email::{self, Email, SimpleEmail};
|
||||||
use cccron_lib::hedgedoc::{self, HedgeDoc};
|
use cccron_lib::hedgedoc::{self, HedgeDoc};
|
||||||
use cccron_lib::is_dry_run;
|
use cccron_lib::is_dry_run;
|
||||||
|
use cccron_lib::key_value::{KeyValueStore as KV, KeyValueStore};
|
||||||
use cccron_lib::mediawiki::{self, Mediawiki};
|
use cccron_lib::mediawiki::{self, Mediawiki};
|
||||||
use cccron_lib::{verboseln,trace_var,trace_var_};
|
|
||||||
use cccron_lib::NYI;
|
use cccron_lib::NYI;
|
||||||
|
|
||||||
/* ***** Config Spec ***** */
|
/* ***** 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"))
|
.map(|v| NaiveDate::parse_from_str(&v, "%F").expect("'TODAY' hat nicht format YYYY-MM-DD"))
|
||||||
.unwrap_or(Local::now().date_naive())
|
.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)]
|
#[derive(Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
@ -170,7 +178,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
&config["wiki-http-password"],
|
&config["wiki-http-password"],
|
||||||
&config["wiki-api-user"],
|
&config["wiki-api-user"],
|
||||||
&config["wiki-api-secret"],
|
&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);
|
trace_var_!(wiki);
|
||||||
// get next plenum days
|
// 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);
|
let nearest_plenum_days = date::get_matching_dates_around(today, plenum_spec);
|
||||||
trace_var!(nearest_plenum_days);
|
trace_var!(nearest_plenum_days);
|
||||||
// figure out where we are
|
// 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 = config.get("state-last-run").unwrap_or_default();
|
||||||
let last_run = NaiveDate::parse_from_str(&last_run, "%Y-%m-%d").unwrap_or_default();
|
let last_run = NaiveDate::parse_from_str(&last_run, "%Y-%m-%d").unwrap_or_default();
|
||||||
trace_var!(last_run);
|
trace_var!(last_run);
|
||||||
|
@ -475,6 +484,7 @@ fn do_protocol(
|
||||||
);
|
);
|
||||||
let _message_id = send_email(&subject, &body, email, config)?;
|
let _message_id = send_email(&subject, &body, email, config)?;
|
||||||
NYI!("convert to mediawiki");
|
NYI!("convert to mediawiki");
|
||||||
|
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
|
||||||
NYI!("add to wiki");
|
NYI!("add to wiki");
|
||||||
config.set("state-name", &ProgramState::Logged.to_string()).ok();
|
config.set("state-name", &ProgramState::Logged.to_string()).ok();
|
||||||
} else {
|
} else {
|
||||||
|
|
464
src/mediawiki.rs
464
src/mediawiki.rs
|
@ -1,12 +1,13 @@
|
||||||
|
use std::cell::OnceCell;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
|
use chrono::{Datelike, NaiveDate, Utc};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use pandoc::{PandocError, PandocOutput};
|
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::{trace_var, verboseln};
|
||||||
use crate::config_spec::{CfgField, CfgGroup};
|
use crate::config_spec::{CfgField, CfgGroup};
|
||||||
|
|
||||||
pub const CONFIG: CfgGroup<'static> = CfgGroup {
|
pub const CONFIG: CfgGroup<'static> = CfgGroup {
|
||||||
|
@ -36,10 +37,15 @@ 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: "plenum-page",
|
||||||
|
default: "Plenum",
|
||||||
|
description: "The name of the wiki page where all new plenum pages will be linked.",
|
||||||
|
},
|
||||||
CfgField::Default {
|
CfgField::Default {
|
||||||
key: "eta",
|
key: "eta",
|
||||||
default: "no ETA, program never ran",
|
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_user: String,
|
||||||
api_secret: String,
|
api_secret: String,
|
||||||
is_dry_run: bool,
|
is_dry_run: bool,
|
||||||
login_token: String,
|
login_token: OnceCell<String>,
|
||||||
csrf_token: String,
|
csrf_token: OnceCell<String>,
|
||||||
|
plenum_main_page_name: String,
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +75,7 @@ impl std::fmt::Debug for Mediawiki {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum RequestType {
|
pub enum ValidRequestTypes {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
PostForEditing
|
PostForEditing
|
||||||
|
@ -76,7 +83,7 @@ pub enum RequestType {
|
||||||
|
|
||||||
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, plenum_main_page_name: &str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server_url: server_url.to_string(),
|
server_url: server_url.to_string(),
|
||||||
|
@ -85,67 +92,104 @@ impl Mediawiki {
|
||||||
api_user: api_user.to_string(),
|
api_user: api_user.to_string(),
|
||||||
api_secret: api_secret.to_string(),
|
api_secret: api_secret.to_string(),
|
||||||
is_dry_run,
|
is_dry_run,
|
||||||
login_token: String::new(),
|
login_token: OnceCell::new(),
|
||||||
csrf_token: String::new(),
|
csrf_token: OnceCell::new(),
|
||||||
|
plenum_main_page_name: plenum_main_page_name.to_string(),
|
||||||
client: Client::builder().cookie_store(true).build().unwrap(),
|
client: Client::builder().cookie_store(true).build().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_login_token(&self) -> Result<String, Box<dyn Error>> {
|
pub fn login (&self) -> Result<(), Box<dyn Error>> {
|
||||||
let url =
|
let url = format!("{}/api.php?", self.server_url);
|
||||||
format!("{}/api.php?", self.server_url);
|
// retrieve login token first
|
||||||
let params: Box<[(&str, &str)]> = Box::from( [
|
let params_0: Box<[(&str, &str)]> = Box::from([
|
||||||
("format", "json"),
|
("action", "query"),
|
||||||
("meta", "tokens"),
|
("meta", "tokens"),
|
||||||
("type", "login"),
|
("type", "login"),
|
||||||
("action", "query")
|
("format", "json")
|
||||||
]);
|
]);
|
||||||
let resp = self.make_request(url, params, RequestType::Get).unwrap();
|
verboseln!("Login params: {:?}", params_0);
|
||||||
let response_deserialized: QueryResponseLogin = serde_json::from_str(&resp)?;
|
let resp_0: String = self.make_request(url.clone(), params_0, ValidRequestTypes::Get)?;
|
||||||
Ok(response_deserialized.query.tokens.logintoken)
|
verboseln!("Raw response login_0: {}", resp_0.yellow());
|
||||||
}
|
let resp_0_deserialized: serde_json::Value = serde_json::from_str(&resp_0)?;
|
||||||
pub fn login (&self) -> Result<String, Box<dyn Error>> {
|
verboseln!("login0 deserialized");
|
||||||
let url = format!("{}/api.php?", self.server_url);
|
|
||||||
let params: Box<[(&str, &str)]> = Box::from([
|
let login_token = resp_0_deserialized
|
||||||
("lgname", self.api_user.as_str()),
|
.pointer("/query/tokens/logintoken")
|
||||||
("lgpassword", self.api_secret.as_str()),
|
.and_then(|v| v.as_str())
|
||||||
("lgtoken", &self.login_token),
|
.map(|token| token.replace("+\\", ""))
|
||||||
("action", "login")
|
.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);
|
verboseln!("Login params: {:?}", params_1);
|
||||||
Ok(resp.unwrap())
|
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())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(format!("Unexpected login response: {}", resp_1_deserialized).into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn get_csrf_token(&self) -> Result<String, Box<dyn Error>> { // HAS TO BE FIXED
|
pub fn get_csrf_token(&self) -> Result<(), Box<dyn Error>> {
|
||||||
let url =
|
let url =
|
||||||
format!("{}/api.php?", self.server_url);
|
format!("{}/api.php?", self.server_url);
|
||||||
let params: Box<[(&str, &str)]> = Box::from([
|
let params: Box<[(&str, &str)]> = Box::from([
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
("meta", "tokens"),
|
("meta", "tokens"),
|
||||||
("formatversion", "2"),
|
|
||||||
("action", "query")
|
("action", "query")
|
||||||
]);
|
]);
|
||||||
let resp: Result<String, Box<dyn Error>> = self.make_request(url, params, RequestType::Get);
|
let resp: String = self.make_request(url, params, ValidRequestTypes::Get)?;
|
||||||
let resp = resp.unwrap();
|
verboseln!("Raw response csrf: {}", resp);
|
||||||
let response_deserialized: QueryResponseCsrf = serde_json::from_str(&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
|
let resp: Result<String, Box<dyn Error>> = match
|
||||||
match request_type {
|
match request_type {
|
||||||
RequestType::Get => {
|
ValidRequestTypes::Get => {
|
||||||
self
|
self
|
||||||
.client
|
.client
|
||||||
.get(url)
|
.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)
|
.query(¶ms)
|
||||||
.send()
|
.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
|
self
|
||||||
.client
|
.client
|
||||||
.post(url)
|
.post(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
|
||||||
.form(¶ms)
|
.query(¶ms_map)
|
||||||
.send()
|
.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,8 +197,8 @@ impl Mediawiki {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
match request_type {
|
match request_type {
|
||||||
RequestType::PostForEditing => Ok(response.text().unwrap()),
|
ValidRequestTypes::PostForEditing => Ok(response.text()?),
|
||||||
_ => Ok(response.text().unwrap())
|
_ => Ok(response.text()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -171,25 +215,267 @@ impl Mediawiki {
|
||||||
};
|
};
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a completely new wiki page with page_content and page_title as inputs
|
/// 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>> {
|
pub fn new_wiki_page(&self, page_title: &str, page_content: &str, update_main_page: bool) -> Result<String, Box<dyn Error>> {
|
||||||
// action=edit&format=json&title=Wikipedia:Sandbox&appendtext=Hello&token=sampleCsrfToken123+\
|
// Prevent dry run from making actual wiki edits
|
||||||
let url =
|
if self.is_dry_run {
|
||||||
format!("{}/api.php?", self.server_url);
|
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([
|
let params: Box<[(&str, &str)]> = Box::from([
|
||||||
("action", "edit"), // Create and edit pages.
|
("action", "edit"),
|
||||||
("format", "json"),
|
("format", "json"),
|
||||||
("title", page_title), // Title of the page to edit. Cannot be used together with pageid.
|
("title", page_title),
|
||||||
("appendtext", page_content), // Add this text to the end of the page or section. Overrides text.
|
("text", page_content),
|
||||||
("token", self.csrf_token.as_str()), // A "csrf" token retrieved from action=query&meta=tokens
|
("token", self.csrf_token.get().unwrap()),
|
||||||
("bot", "true"), // Mark this edit as a bot edit.
|
("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)]
|
#[derive(Deserialize)]
|
||||||
struct QueryResponseLogin {
|
struct QueryResponseLogin {
|
||||||
|
#[allow(dead_code)]
|
||||||
batchcomplete: String,
|
batchcomplete: String,
|
||||||
query: QueryTokensLogin,
|
query: QueryTokensLogin,
|
||||||
}
|
}
|
||||||
|
@ -206,7 +492,7 @@ struct TokensLogin {
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct QueryResponseCsrf {
|
struct QueryResponseCsrf {
|
||||||
batchcomplete: bool,
|
#[allow(dead_code)]
|
||||||
query: crate::mediawiki::QueryTokensCsrf,
|
query: crate::mediawiki::QueryTokensCsrf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,75 +505,3 @@ struct QueryTokensCsrf {
|
||||||
struct TokensCsrf {
|
struct TokensCsrf {
|
||||||
csrftoken: String,
|
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