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}} +
+

{{this.title.content}}

+

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(()) +}