From 8bc7556cf8b868aa2f713445c195107fa873a7d0 Mon Sep 17 00:00:00 2001
From: Semubico <>
Date: Thu, 3 Jul 2025 15:16:21 +0300
Subject: [PATCH] First working version
---
.gitignore | 2 +
Cargo.toml | 23 +++++++++
assets/index.html | 80 ++++++++++++++++++++++++++++++
assets/item.html | 11 +++++
config.toml | 7 +++
src/main.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 246 insertions(+)
create mode 100644 .gitignore
create mode 100644 Cargo.toml
create mode 100644 assets/index.html
create mode 100644 assets/item.html
create mode 100644 config.toml
create mode 100644 src/main.rs
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0132c2b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/feeds
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..18e8763
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "rssserver"
+version = "0.1.0"
+edition = "2024"
+
+[profile.release]
+strip = true
+opt-level = "s"
+lto = "on"
+codegen-units = 1
+
+[dependencies]
+anyhow = { version = "1.0.98", default-features = false, features = ["std"] }
+axum = { version = "0.8.4", features = ["http2"] }
+chrono = "0.4.41"
+env_logger = "0.11.8"
+feed-rs = "2.3.1"
+handlebars = "6.3.2"
+log = "0.4.27"
+reqwest = { version = "0.12.22", features = ["charset", "h2", "http2", "rustls-tls", "system-proxy"], default-features = false }
+serde = { version = "1.0.219", features = ["derive"] }
+tokio = { version = "1.46.0", features = ["full"] }
+toml = "0.8.23"
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..eb7b099
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+ MyReadingRSS
+
+
+
+ RSS feeds I read
+
+
+
+
+
+
diff --git a/assets/item.html b/assets/item.html
new file mode 100644
index 0000000..8cf7fe5
--- /dev/null
+++ b/assets/item.html
@@ -0,0 +1,11 @@
+{{title.content}}
+
+ {{#each entries}}
+
+
+
By: {{this.authors.0.name}}
+
Published on:
+
{{summary.content}}
+
+ {{/each}}
+
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..f7e3c9b
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,7 @@
+bind = "127.0.0.1:8085"
+item_tpl = "./assets/item.html"
+page_tpl = "./assets/index.html"
+
+[feeds.tc]
+url = "https://www.youtube.com/feeds/videos.xml?channel_id=UCy0tKL1T7wFoYcxCe0xjN6Q"
+interval_secs = 43200
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..455f01f
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,123 @@
+use reqwest;
+
+#[derive(Debug, Clone, serde::Deserialize)]
+struct FeedConfig {
+ interval_secs: Option,
+ url: String
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+struct AppConfig {
+ bind: String,
+ item_tpl: String,
+ page_tpl: String,
+ feeds: std::collections::HashMap
+}
+
+#[derive(Debug, Clone)]
+struct AppState<'a> {
+ config: AppConfig,
+ templater: handlebars::Handlebars<'a>,
+}
+
+fn read_config() -> anyhow::Result {
+ let config_path = std::env::var("RSS_CONFIG").unwrap_or("./config.toml".into());
+ let cfg = std::fs::read_to_string(&config_path)?;
+ let mut cfg: AppConfig = toml::from_str(&cfg)?;
+ Ok(cfg)
+}
+
+fn init_template_engine(cfg: AppConfig) -> anyhow::Result> {
+ let mut hb = handlebars::Handlebars::new();
+
+ let page_tpl = std::fs::read_to_string(&cfg.page_tpl)?;
+ hb.register_template_string("page", page_tpl)?;
+
+ let item_tpl = std::fs::read_to_string(&cfg.item_tpl)?;
+ hb.register_template_string("item", item_tpl)?;
+
+ Ok(AppState {
+ config: cfg,
+ templater: hb
+ })
+}
+
+async fn file_needs_update(
+ path: &std::path::Path,
+ expiration_timeout: std::time::Duration
+) -> anyhow::Result {
+ let meta = tokio::fs::metadata(path).await?;
+ let time = meta.modified()?;
+ Ok(time.elapsed()?.as_secs() >= expiration_timeout.as_secs())
+}
+
+async fn get_rss(name: &str, cfg: &FeedConfig) -> anyhow::Result {
+
+ let filename = std::path::PathBuf::from(format!("./feeds/{}.last_feed", name));
+ let data;
+ if file_needs_update(&filename, std::time::Duration::from_secs(cfg.interval_secs.unwrap_or(0))).await.unwrap_or(true) {
+ data = reqwest::get(cfg.url.clone()).await?.text().await?;
+ let _ = tokio::fs::write(&filename, &data).await?;
+ }
+ else {
+ data = tokio::fs::read_to_string(&filename).await?;
+ }
+
+ let feed = feed_rs::parser::parse(data.as_bytes())?;
+ Ok(feed)
+}
+
+async fn render_rss_feeds(axum::extract::State(state): axum::extract::State>>) -> impl axum::response::IntoResponse {
+ let mut res = Vec::new();
+ for (name, cfg) in state.config.feeds.iter() {
+
+ let feed = match get_rss(&name, cfg).await {
+ Err(e) => {
+ res.push(format!("Error fetching feed: {}", &name));
+ log::error!("Error fetching feed: {}", e);
+ continue;
+ },
+ Ok(feed) => feed,
+ };
+
+ let html = match render_rss_feed(feed, state.clone()).await {
+ Err(e) => {
+ res.push(format!("Error rendering feed: {}", &name));
+ log::error!("Error rendering feed: {}", e);
+ continue;
+ }
+ Ok(html) => html
+ };
+
+ res.push(html);
+ }
+
+ let res = res.join("");
+ let mut hm = std::collections::BTreeMap::new();
+ hm.insert("content", res);
+ let html = state.templater.render("page", &hm).unwrap();
+
+ axum::response::Response::builder()
+ .status(axum::http::StatusCode::OK)
+ .header("Content-Type", "text/html")
+ .body(axum::body::Body::from(html))
+ .unwrap()
+}
+
+async fn render_rss_feed(feed: feed_rs::model::Feed, state: std::sync::Arc>) -> anyhow::Result {
+ Ok(state.templater.render("item", &feed)?)
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ env_logger::init();
+ let conf = read_config()?;
+ let state = init_template_engine(conf)?;
+ let bind = &state.config.bind;
+ let list = tokio::net::TcpListener::bind(bind).await?;
+ let serv = axum::Router::new().route("/", axum::routing::get(render_rss_feeds)).with_state(std::sync::Arc::new(state));
+
+ axum::serve(list, serv).await?;
+
+ Ok(())
+}