diff options
| author | Fuwn <[email protected]> | 2022-07-08 23:58:29 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-07-08 23:58:29 -0700 |
| commit | 3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3 (patch) | |
| tree | 13bd1dcac64e5048a45c42fc98dee3cd951cebcc | |
| parent | fix(content): site title (diff) | |
| download | locus-3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3.tar.xz locus-3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3.zip | |
feat(blog): rss feed for blogs
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/macros.rs | 2 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/modules/blog/module.rs | 79 | ||||
| -rw-r--r-- | src/xml.rs | 98 |
5 files changed, 176 insertions, 7 deletions
@@ -21,9 +21,10 @@ codegen-units = 1 [dependencies] tokio = { version = "0.2.4", features = ["full"] } # Asynchronous Runtime +chrono = "0.4.19" # Date and Time pickledb = "0.4.1" # Database tantivy = "0.17.0" # Full-text Search Engine -windmark = { version = "0.1.16", features = [ +windmark = { version = "0.1.19", features = [ "logger", "auto-deduce-mime" ] } # Gemini Server Framework diff --git a/src/macros.rs b/src/macros.rs index 5a2a01c..724ef5e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -85,7 +85,7 @@ macro_rules! mount_file { ($router).mount( $at, Box::new(|_| { - windmark::Response::SuccessFile(include_bytes!(concat!( + windmark::Response::SuccessFileAuto(include_bytes!(concat!( "../../content/meta/", $file ))) diff --git a/src/main.rs b/src/main.rs index dcefb64..fb4737a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ mod macros; mod modules; mod route; +mod xml; #[macro_use] extern crate log; diff --git a/src/modules/blog/module.rs b/src/modules/blog/module.rs index 62152bf..4ccf59e 100644 --- a/src/modules/blog/module.rs +++ b/src/modules/blog/module.rs @@ -22,7 +22,12 @@ use std::{ io::Read, }; -use crate::{modules::blog::config::Blog, route::track_mount, success}; +use crate::{ + modules::blog::config::Blog, + route::track_mount, + success, + xml::{Item as XmlItem, Writer as XmlWriter}, +}; #[allow(clippy::too_many_lines)] pub fn module(router: &mut windmark::Router) { @@ -156,6 +161,7 @@ pub fn module(router: &mut windmark::Router) { for (blog, mut entries) in blogs { let fixed_blog_name = blog.replace(' ', "_").to_lowercase(); let fixed_blog_name_clone = fixed_blog_name.clone(); + let fixed_blog_name_clone_2 = fixed_blog_name.clone(); let config: Option<Blog> = entries.remove_entry("blog.json").and_then(|(_, content)| { if let Ok(config) = Blog::from_string(&content) { @@ -178,15 +184,29 @@ pub fn module(router: &mut windmark::Router) { .clone() .unwrap_or_else(|| "One of Fuwn's blogs".to_string()); let config_clone = config.clone(); + let mut xml = XmlWriter::builder(); + + xml.add_field("title", &name); + xml.add_field( + "link", + &format!("gemini://fuwn.me/blog/{}", fixed_blog_name), + ); + xml.add_field("description", &description); + xml.add_field("generator", "locus"); + xml.add_field("lastBuildDate", &chrono::Local::now().to_rfc2822()); + xml.add_link(&format!("gemini://fuwn.me/blog/{}.xml", fixed_blog_name)); track_mount( router, &format!("/blog/{}", fixed_blog_name), &format!("{} ― {}", name, description), Box::new(move |context| { + let fixed_blog_name = fixed_blog_name_clone.clone(); + success!( &format!( - "# {} ({})\n\n{}\n\n{}", + "# {} ({})\n\n{}\n\n{}\n\n## Really Simple Syndication\n\nAccess \ + {0}'s RSS feed\n\n=> {} here!", blog, entries_clone.len(), description, @@ -200,7 +220,7 @@ pub fn module(router: &mut windmark::Router) { "=> {} {}{}", format_args!( "/blog/{}/{}", - fixed_blog_name_clone, + fixed_blog_name, title.to_lowercase() ), title, @@ -225,7 +245,8 @@ pub fn module(router: &mut windmark::Router) { ) }) .collect::<Vec<_>>() - .join("\n") + .join("\n"), + format_args!("/blog/{}.xml", fixed_blog_name), ), context ) @@ -238,6 +259,42 @@ pub fn module(router: &mut windmark::Router) { } else { ("".to_string(), "".to_string()) }; + let fixed_blog_name = fixed_blog_name_clone_2.clone(); + + xml.add_item(&{ + let mut builder = XmlItem::builder(); + + builder.add_field("title", &title); + builder.add_field( + "link", + &format!( + "gemini://fuwn.me/blog/{}/{}", + fixed_blog_name, + title.to_lowercase() + ), + ); + builder.add_field("description", &contents); + builder.add_field( + "guid", + &format!( + "gemini://fuwn.me/blog/{}/{}", + fixed_blog_name, + title.to_lowercase() + ), + ); + + if let Some(configuration) = &config { + if let Some(posts) = configuration.posts() { + if let Some(post) = posts.get(&title) { + if let Some(date) = post.created() { + builder.add_field("pubDate", date); + } + } + } + } + + builder + }); track_mount( router, @@ -253,10 +310,22 @@ pub fn module(router: &mut windmark::Router) { } ), Box::new(move |context| { - success!(format!("{}\n{}", header.0, contents), context) + success!(format!("{}\n{}", header.0, contents,), context) }), ); } + + track_mount( + router, + &format!("/blog/{}.xml", fixed_blog_name), + &format!("Really Simple Syndication for the {} blog", name), + Box::new(move |_| { + windmark::Response::SuccessWithMime( + xml.to_string(), + "text/rss+xml".to_string(), + ) + }), + ); } } diff --git a/src/xml.rs b/src/xml.rs new file mode 100644 index 0000000..0709723 --- /dev/null +++ b/src/xml.rs @@ -0,0 +1,98 @@ +// This file is part of Locus <https://github.com/gemrest/locus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::{collections::HashMap, fmt::Display}; + +pub struct Item { + fields: HashMap<String, String>, +} +impl Item { + pub fn builder() -> Self { + Self { + fields: HashMap::new(), + } + } + + pub fn add_field(&mut self, key: &str, value: &str) -> &mut Self { + self.fields.insert(key.to_string(), value.to_string()); + + self + } +} +impl Display for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "<item>{}</item>", + self.fields.iter().fold(String::new(), |mut acc, (k, v)| { + acc.push_str(&format!("<{}>{}</{}>", k, v, k)); + acc + }) + ) + } +} + +#[derive(Clone)] +pub struct Writer { + content: String, + fields: HashMap<String, String>, + link: String, +} +impl Writer { + pub fn builder() -> Self { + Self { + content: String::new(), + fields: HashMap::default(), + link: "".to_string(), + } + } + + pub fn add_link(&mut self, link: &str) -> &mut Self { + self.link = link.to_string(); + + self + } + + pub fn add_field(&mut self, key: &str, value: &str) -> &mut Self { + self.fields.insert(key.to_string(), value.to_string()); + + self + } + + pub fn add_item(&mut self, item: &Item) -> &mut Self { + self.content.push_str(&item.to_string()); + + self + } +} +impl Display for Writer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss xmlns:atom=\"http://www.w3.org/2005/Atom\" \ + version=\"2.0\"><channel>{}<atom:link href=\"{}\" rel=\"self\" \ + type=\"application/rss+xml\" />{}</channel></rss>", + self.fields.iter().fold(String::new(), |mut acc, (k, v)| { + acc.push_str(&format!("<{}>{}</{}>", k, v, k)); + acc + }), + self.link, + self.content + ) + } +} |