use crate::config_spec::{CfgField, CfgGroup}; use regex::Regex; use reqwest::blocking::Client; use reqwest::blocking::Response; use std::error::Error; pub const CONFIG: CfgGroup<'static> = CfgGroup { name: "hedgedoc", description: "HedgeDoc markdown pad server settings", fields: &[ CfgField::Default { key: "server-url", default: "https://md.berlin.ccc.de", description: "Hedgedoc server storing the pads.", }, CfgField::Default { key: "template-name", default: "plenum-template", description: "Name of the pad containing the template to use.", }, CfgField::Generated { key: "last-id", generator: make_pad_id, generator_description: "Makes a new pad that's completely empty.", description: "ID of last plenum's pad.", }, CfgField::Generated { key: "next-id", generator: make_pad_id, generator_description: "Makes a new pad that's completely empty.", description: "ID of next plenum's pad.", }, ], }; #[derive(Debug)] pub struct HedgeDoc { server_url: String, is_dry_run: bool, client: Client, } impl HedgeDoc { pub fn new(server_url: &str, is_dry_run: bool) -> Self { Self { server_url: server_url.to_string(), is_dry_run, client: Client::new() } } pub fn format_url(&self, pad_name: &str) -> String { format!("{}/{}", self.server_url, pad_name) } fn format_action(&self, pad_name: &str, verb: &str) -> String { format!("{}/{}/{}", self.server_url, pad_name, verb) } fn do_request(&self, url: &str) -> Result> { 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 { res.url().to_string().trim_start_matches(&format!("{}/", self.server_url)).to_string() } pub fn download(&self, pad_name: &str) -> Result> { Ok(self.do_request(&self.format_action(pad_name, "download"))?.text()?) } pub fn create_pad(&self) -> Result> { if self.is_dry_run { todo!("NYI: sane dry-run behavior") } let res = self.do_request(&format!("{}/new", self.server_url)).unwrap(); if res.status().is_success() { Ok(self.get_id_from_response(res)) } else { Err(format!("Failed to create pad {}", res.status()).into()) } } pub fn import_note(&self, id: Option<&str>, content: String) -> Result> { if self.is_dry_run { todo!("NYI: sane dry-run behavior") } let url = match id { Some(id) => self.format_url(&format!("new/{id}")), None => self.format_url("new"), }; let res = self.client.post(&url).header("Content-Type", "text/markdown").body(content).send()?; if res.status().is_success() { Ok(self.get_id_from_response(res)) } else { Err(format!("Failed to import note: {}", res.status()).into()) } } } pub fn extract_metadata(pad_content: String) -> String { let re_yaml = Regex::new(r"(?s)---\s*(.*?)\s*(?:\.\.\.|---)").unwrap(); re_yaml.captures_iter(&pad_content).map(|c| c[1].to_string()).collect::>().join("\n") } pub fn strip_metadata(pad_content: String) -> String { let re_yaml = Regex::new(r"(?s)---\s*.*?\s*(?:\.\.\.|---)").unwrap(); let pad_content = re_yaml.replace_all(&pad_content, "").to_string(); let re_comment = Regex::new(r"(?s)").unwrap(); let content_without_comments = re_comment.replace_all(&pad_content, "").to_string(); content_without_comments.trim().to_string() } pub fn summarize(pad_content: String) -> String { // 1. remove HTML comments let pad_content = strip_metadata(pad_content); // 2. accumulate topic lines let re_header = Regex::new(r"^\s*##(#*) TOP ([\d.]+\s*.*?)\s*#*$").unwrap(); let mut result: Vec = Vec::new(); for line in pad_content.lines() { if let Some(captures) = re_header.captures(line) { let indent = " ".repeat(captures.get(1).unwrap().as_str().len()); let title = captures.get(2).unwrap().as_str(); result.push(format!("{}{}", indent, title)); } } result.join("\n") } /// For the config, make a new pad ID (by actually making a pad.) fn make_pad_id( _key: &str, config: &crate::key_value::KeyValueStore, is_dry_run: bool, ) -> Result> { HedgeDoc::new(&config.get("hedgedoc-server-url").unwrap(), is_dry_run).create_pad() }