diff --git a/.gitignore b/.gitignore index a176ca3..898fa68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target /archive -/archive.html +/public diff --git a/src/main.rs b/src/main.rs index 0d6200e..317c598 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use jiff::tz::TimeZone; use jiff::Timestamp; use pleroma::Activities; -use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; use std::sync::OnceLock; use std::{ collections::HashMap, - env, fmt, + env, fs, fs::File, io::{self, BufReader, BufWriter}, path::{Path, PathBuf}, @@ -18,19 +18,25 @@ mod pleroma; type BoxError = Box; type Mappings = HashMap>; +const STYLE: &str = include_str!("../style.css"); + static TZ: OnceLock = OnceLock::new(); static MAPPINGS: OnceLock = OnceLock::new(); fn main() -> ExitCode { - let Some(path) = env::args_os().skip(1).next().map(PathBuf::from) else { - eprintln!("Usage: pleroma-archive path/to/pleroma-archive"); - return ExitCode::FAILURE; + let args = env::args_os().skip(1).collect::>(); + let (archive_path, output_path) = match args.as_slice() { + [archive, output] => (Path::new(archive), Path::new(output)), + _ => { + eprintln!("Usage: pleroma-archive path/to/pleroma-archive output/path"); + return ExitCode::FAILURE; + } }; let timezone = TimeZone::system(); TZ.set(timezone).unwrap(); - match try_main(&path) { + match try_main(archive_path, output_path) { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("Error: {err}"); @@ -39,7 +45,7 @@ fn main() -> ExitCode { } } -fn try_main(path: &Path) -> Result<(), BoxError> { +fn try_main(path: &Path, output_path: &Path) -> Result<(), BoxError> { let actor_path = path.join("actor.json"); let outbox_path = path.join("outbox.json"); let mappings_path = path.join("mappings.json"); @@ -57,8 +63,14 @@ fn try_main(path: &Path) -> Result<(), BoxError> { Err(err) => return Err(err.into()), }; + // Ensure output path exists, and write out stylesheet + fs::create_dir_all(output_path)?; + let style_path = output_path.join("style.css"); + fs::write(&style_path, STYLE)?; + let agent = ureq::AgentBuilder::new().redirects(0).build(); + // Process posts let mut posts = Vec::with_capacity(activities.ordered_items.len()); for item in &activities.ordered_items { if item.direct_message { @@ -109,42 +121,91 @@ fn try_main(path: &Path) -> Result<(), BoxError> { MAPPINGS.set(mappings).unwrap(); - // dbg!(&posts); - - println!( - "{}", - Page { - title: "Pleroma Archive", + // Generate index.html + let index_html = Layout { + title: "Pleroma Archive", + body: Index { actor: &actor, activities: &posts, + }, + actor: &actor, + } + .to_string(); + + let index_path = output_path.join("index.html"); + println!("Writing {}", index_path.display()); + fs::write(&index_path, index_html.as_bytes())?; + + // Generate individual post pages + for post in posts { + let Some(Some(url)) = MAPPINGS.get().unwrap().get(&post.id) else { + continue; + }; + let mut post_path = output_path + .iter() + .chain( + url.path_segments() + .ok_or_else(|| BoxError::from("unable to get path segments of {url}"))? + .map(OsStr::new), + ) + .collect::(); + post_path.set_extension("html"); + + let post_html = Layout { + title: &format!( + "Post from {} on {}", + actor.preferred_username, + post.human_published() + ), + body: Show { + actor: &actor, + activity: post, + }, + actor: &actor, } - ); + .to_string(); + + println!("Writing {}", post_path.display()); + fs::create_dir_all(&post_path.parent().expect("post has parent dir"))?; + fs::write(&post_path, post_html.as_bytes())?; + } Ok(()) } markup::define! { - Page<'a>(title: &'a str, actor: &'a pleroma::Actor, activities: &'a [&'a pleroma::Activity]) { + Layout<'a, Body: markup::Render>(title: &'a str, body: Body, actor: &'a pleroma::Actor) { @markup::doctype() html { head { meta[charset="utf-8"]; meta[name="viewport", content="width=device-width, initial-scale=1"]; title { @title } - link[rel="stylesheet", type="text/css", href="style.css"]; + link[rel="stylesheet", type="text/css", href="/style.css"]; } body { @Header { title, actor } - main { - @for activity in activities.iter().rev() { - @Activity { actor, activity } - } - } + @body @Footer { } } } } + Index<'a>(actor: &'a pleroma::Actor, activities: &'a [&'a pleroma::Activity]) { + main { + p { @activities.len() " posts:" } + @for activity in activities.iter().rev() { + @Activity { actor, activity } + } + } + } + + Show<'a>(actor: &'a pleroma::Actor, activity: &'a pleroma::Activity) { + a[href="/"] { "☜ Back to home page" } + + @Activity { actor, activity } + } + Header<'a>(title: &'a str, actor: &'a pleroma::Actor) { header { h1 { @title } @@ -165,7 +226,7 @@ markup::define! { @Actor { actor } div[class="activity-content"] { - a[href=MAPPINGS.get().unwrap().get(&activity.id).and_then(|url| url.as_ref().map(|url| url.as_str()))] { + a[href=MAPPINGS.get().unwrap().get(&activity.id).and_then(|url| url.as_ref().map(|url| url.path()))] { time[datetime=&activity.published] { @activity.human_published() } } diff --git a/src/pleroma/activity.rs b/src/pleroma/activity.rs index 58f5a65..cd65ae9 100644 --- a/src/pleroma/activity.rs +++ b/src/pleroma/activity.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] diff --git a/src/pleroma/actor.rs b/src/pleroma/actor.rs index 855f41f..43d8321 100644 --- a/src/pleroma/actor.rs +++ b/src/pleroma/actor.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use serde::Deserialize; #[derive(Debug, Clone, Deserialize)]