aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2022-07-08 23:58:29 -0700
committerFuwn <[email protected]>2022-07-08 23:58:29 -0700
commit3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3 (patch)
tree13bd1dcac64e5048a45c42fc98dee3cd951cebcc
parentfix(content): site title (diff)
downloadlocus-3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3.tar.xz
locus-3634108c9f4c6e8f01e85f4b06d2a3c78f300fc3.zip
feat(blog): rss feed for blogs
-rw-r--r--Cargo.toml3
-rw-r--r--src/macros.rs2
-rw-r--r--src/main.rs1
-rw-r--r--src/modules/blog/module.rs79
-rw-r--r--src/xml.rs98
5 files changed, 176 insertions, 7 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 478c966..d0d8c81 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
+ )
+ }
+}