From 659e9f258aedc3fb7efa0b6928deb74258232a6a Mon Sep 17 00:00:00 2001 From: murmeldin Date: Wed, 27 Nov 2024 20:34:07 +0100 Subject: [PATCH] mediawiki work in progress --- src/main.rs | 5 +- src/mediawiki.rs | 279 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 191 insertions(+), 93 deletions(-) diff --git a/src/main.rs b/src/main.rs index e1bd3a5..5bea989 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,10 @@ 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 cccron_lib::{trace_var, trace_var_, verboseln}; use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec}; use cccron_lib::date; @@ -485,7 +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)?; + mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?; NYI!("add to wiki"); config.set("state-name", &ProgramState::Logged.to_string()).ok(); } else { diff --git a/src/mediawiki.rs b/src/mediawiki.rs index fc837d8..d43971e 100644 --- a/src/mediawiki.rs +++ b/src/mediawiki.rs @@ -1,7 +1,7 @@ use std::cell::OnceCell; use std::error::Error; -use chrono::{Datelike, Utc}; +use chrono::{Datelike, NaiveDate, Utc}; use colored::Colorize; use reqwest::blocking::Client; use serde::Deserialize; @@ -183,89 +183,105 @@ impl Mediawiki { } /// 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> { - // 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. - ("text", page_content), // Add this text to the end of the page or section. Overrides text. - ("token", self.csrf_token.get().unwrap()), // A "csrf" token retrieved from action=query&meta=tokens - ("bot", "true"), // Mark this edit as a bot edit. - ]); - - println!("Making request to {} with params: {:?}", url, params); - - let request_result = self.make_request(url, params, ValidRequestTypes::Post); - - if update_main_page { - self.update_plenum_page(page_title)?; + pub fn new_wiki_page(&self, page_title: &str, page_content: &str, update_main_page: bool) -> Result> { + // 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"), + ("format", "json"), + ("title", page_title), + ("text", page_content), + ("token", self.csrf_token.get().unwrap()), + ("createonly", "true"), // Prevent overwriting existing pages + ("bot", "true"), + ]); + + // 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()) } - request_result } + /// 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> { - // 1. Get the current year: + pub fn update_plenum_page(&self, new_page_title_to_link_to: &str) -> Result<(), Box> { let current_year = Utc::now().year().to_string(); - let year_pattern = format!("=== {} ===", current_year); - - // 2. Download Plenum page content - let page_content = self.get_page_content(&self.plenum_main_page_name)?; - //trace_var!(page_content); - - // 3. Check if the current year exists - let mut updated_section = String::new(); - if page_content.contains(&year_pattern) { - // 4. Year Heading exists, inserting new link below - let mut content_split: Vec<&str> = page_content.split(&year_pattern).collect(); - let site_content_below_year_heading = content_split.pop().unwrap_or_default(); - let site_content_above_year_heading = content_split.pop().unwrap_or_default(); - updated_section = format!( - "{}{}\n* [[{}]]{}", - site_content_above_year_heading, - year_pattern, - new_page_title_to_link_to, - site_content_below_year_heading); - verboseln!("updated page: {}", updated_section) - } else { - // Scenario: New year: Generating new heading for the current year - // 4. First checking if the last year exists: - let last_year = current_year.parse::()? - 1; // returns e.g. 2023 when given 2024 - let last_year_pattern = format!("=== {} ===", last_year); - if page_content.contains(&last_year_pattern) { - // 2. Secondly, generate a new heading for this year above last year's - verboseln!("new year, adding this year's heading"); - let mut content_split: Vec<&str> = page_content.split(&last_year_pattern).collect(); - let site_content_below_year_heading = content_split.pop().unwrap_or_default(); - let site_content_above_year_heading = content_split.pop().unwrap_or_default(); - updated_section = format!( - "{}{}\n* [[{}]]\n\n{}{}", - site_content_above_year_heading, - year_pattern, - new_page_title_to_link_to, - last_year_pattern, - site_content_below_year_heading); + 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::()? - 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 { - println!("There is neither a {}, nor a {} heading on the main plenum Page!", current_year, last_year); - panic!("The Plenum Main Page seems seriously damaged, aborting...") + // 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) + ); } - // uploading the pad again - self.new_wiki_page(&self.plenum_main_page_name, &updated_section, false)?; // nicht auf true setzen, sonst entsteht eine Endlosschleife + + // 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> { - let url = - format!("{}/api.php?", self.server_url); + pub fn get_page_content(&self, page_title: &str) -> Result> { + let url = format!("{}/api.php?", self.server_url); let params: Box<[(&str, &str)]> = Box::from([ - ("action", "parse"), // Create and edit pages. + ("action", "parse"), ("prop", "wikitext"), ("format", "json"), ("page", page_title), @@ -273,26 +289,118 @@ impl Mediawiki { ]); let resp = self.make_request(url, params, ValidRequestTypes::Get)?; let response_deserialized: serde_json::Value = serde_json::from_str(&resp)?; - - let resp = response_deserialized + + let wikitext = response_deserialized .get("parse") - .and_then(|parse | parse.get("wikitext")) - .ok_or("Expected field `wikitext` not found, likely no access to main plenum page")?; - Ok(resp.to_string()) + .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> { + // 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 + self.get_login_token()?; + 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::(&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) -> Result<(), Box> { +pub fn pad_ins_wiki(old_pad_content: String, wiki: &Mediawiki, plenum_date: &NaiveDate) -> Result<(), Box> { + // 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 wiki.get_login_token()?; verboseln!("Login token acquired."); + let login_result = wiki.login()?; verboseln!("Login done."); trace_var!(login_result); + wiki.get_csrf_token()?; verboseln!("CSRF token acquired."); @@ -301,17 +409,12 @@ pub fn pad_ins_wiki(old_pad_content: String, wiki: &Mediawiki) -> Result<(), Box trace_var!(pad_converted); // Create a new wiki page plenum_main_page_name/page_title, e.g. under Plenum/13._August_2024 - // and past the converted pad there verboseln!("wiki: uploading converted pad"); - // let page_title = "14._August_2024"; - let page_title = "14._AUGUST_2024"; - let page_title = format!("{}/{}", wiki.plenum_main_page_name, page_title); - wiki.new_wiki_page(&page_title, &pad_converted, true)?; + 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)?; - // Download the main plenum page and insert the new page_title as a link - verboseln!("wiki: updating main plenum page"); - - wiki.update_plenum_page(&page_title)?; verboseln!("Finished successfully with wiki"); Ok(()) } @@ -332,14 +435,10 @@ fn pandoc_convert(markdown: String) -> Result> { } } -/* -fn create_title (nächster_plenumstermin: String) { - let date_simple = NaiveDate::from(nächster_plenumstermin); - let wiki_page_title = format!("{} {} {}", date_simple.day(), LongMonthName[date_simple.month()], date_simple.year()); +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