From c9f8f8f2764637a042ba19746899456819d4e0bb Mon Sep 17 00:00:00 2001 From: imnyang Date: Thu, 15 Jan 2026 06:12:12 +0900 Subject: [PATCH] Add support for image uploads and link filtering in RSS feeds --- .env.example | 2 ++ Cargo.lock | 1 + Cargo.toml | 1 + README.md | 2 +- compose.yml | 1 + rss.toml.example | 14 ++++++++++++++ src/config.rs | 3 +++ src/main.rs | 34 +++++++++++++++++++++++++++++++++- src/rss.rs | 44 ++++++++++++++++++++++++++++++++++++++------ 9 files changed, 94 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 1057b49..4a14c29 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ DISCORD_TOKEN=your_discord_bot_token_here +RSS_USER_AGENT=your_custom_user_agent_here +INTERVAL_MINUTES=5 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index de45ce3..eca91e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,6 +1033,7 @@ dependencies = [ "dotenvy", "feed-rs", "htmd", + "regex", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 83d392b..cbbd428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ anyhow = "1.0" chrono = "0.4" sled = "0.34" dotenvy = "0.15" +regex = "1" diff --git a/README.md b/README.md index d07ca48..a82c456 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Discord RSS monitor written in Rust. 1. Create a `.env` file with: ``` DISCORD_TOKEN=your_token - INTERVAL_MINUTES=5 + INTERVAL_MINUTES=15 ``` 2. Configure `rss.toml`. 3. Run with `cargo run`. diff --git a/compose.yml b/compose.yml index 47bc7da..07671d6 100644 --- a/compose.yml +++ b/compose.yml @@ -7,3 +7,4 @@ services: volumes: - ./data:/usr/app/data - ./rss.toml:/usr/app/rss.toml + - /etc/localtime:/etc/localtime:ro diff --git a/rss.toml.example b/rss.toml.example index 3a82609..fabc365 100644 --- a/rss.toml.example +++ b/rss.toml.example @@ -25,3 +25,17 @@ link = "link" content = "description" author = "dc:creator" pubDate = "pubDate" + +[quasarzone] +channel = "1448338163864174867" +tag = "1461101612960583753" +emoji = "๐Ÿช" +rss = "https://quasarzone.com/rss.xml" +link_filter = ["^https?:\/\/[^\/]+\/bbs\/qn_game(\/|$)", "^https?:\/\/[^\/]+\/bbs\/qn_partner(\/|$)"] +upload_image = true + +[quasarzone.setup] +title = "title" +link = "link" +content = "description" +author = "dc:creator" diff --git a/src/config.rs b/src/config.rs index 6c6d8b8..80a7bf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,9 @@ pub struct RssConfig { pub emoji: String, pub rss: String, pub category_filter: Option>, + pub link_filter: Option>, + #[serde(default)] + pub upload_image: bool, pub setup: Option, } diff --git a/src/main.rs b/src/main.rs index f6f0d9a..0ac6241 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,16 @@ async fn main() -> Result<()> { Ok(()) } +async fn download_image(url: &str) -> Result> { + let client = reqwest::Client::builder() + .user_agent(std::env::var("RSS_USER_AGENT").unwrap_or_else(|_| "NekoRSS/1.0".to_string())) + .build()?; + + let response = client.get(url).send().await?; + let data = response.bytes().await?.to_vec(); + Ok(data) +} + async fn check_feed( name: &str, config: &crate::config::RssConfig, @@ -116,10 +126,32 @@ async fn check_feed( let title = rss::get_field_value(&item, config, "title").unwrap_or_else(|| "Untitled".to_string()); let tag_id = config.tag.parse::()?; + // Extract image if needed + let image_url = if config.upload_image { + rss::extract_image_url(&item) + } else { + None + }; + + // Create message with image if available + let mut message_builder = serenity::builder::CreateMessage::new().content(content); + if let Some(img_url) = image_url { + match download_image(&img_url).await { + Ok(image_data) => { + message_builder = message_builder.add_file( + serenity::all::CreateAttachment::bytes(image_data, "image.jpg") + ); + } + Err(e) => { + eprintln!("[{}] Failed to download image: {}", name, e); + } + } + } + // Post to forum let post = serenity::builder::CreateForumPost::new( title, - serenity::builder::CreateMessage::new().content(content) + message_builder ).add_applied_tag(serenity::all::ForumTagId::new(tag_id)); if let Err(e) = channel.create_forum_post(&http, post).await { diff --git a/src/rss.rs b/src/rss.rs index 449f802..2016e65 100644 --- a/src/rss.rs +++ b/src/rss.rs @@ -2,10 +2,11 @@ use crate::config::RssConfig; use anyhow::Result; use htmd::HtmlToMarkdown; use feed_rs::model::{Feed, Entry}; +use regex::Regex; pub async fn fetch_feed(url: &str) -> Result { let client = reqwest::Client::builder() - .user_agent("NekoRSS/1.0 (+abuse@imnya.ng)") + .user_agent(std::env::var("RSS_USER_AGENT").unwrap_or_else(|_| "NekoRSS/1.0 (+https://github.com/imnyang/memos-rss)".to_string())) .build()?; let resp = client.get(url).send().await?; @@ -21,6 +22,33 @@ pub async fn fetch_feed(url: &str) -> Result { } } +pub fn should_include_by_link(link: Option<&String>, link_filters: Option<&Vec>) -> bool { + match (link, link_filters) { + (Some(link_url), Some(filters)) => { + // ๋ชจ๋“  ํ•„ํ„ฐ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งค์น˜๋˜๋ฉด ํฌํ•จ + filters.iter().any(|filter| { + if let Ok(regex) = Regex::new(filter) { + regex.is_match(link_url) + } else { + false + } + }) + } + (_, None) => true, // ํ•„ํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋‘ ํฌํ•จ + (None, Some(_)) => false, // ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”๋ฐ link๊ฐ€ ์—†์œผ๋ฉด ์ œ์™ธ + } +} + +pub fn extract_image_url(item: &Entry) -> Option { + // ์ฒซ ๋ฒˆ์งธ๋กœ media:content์—์„œ ์ด๋ฏธ์ง€ ์ฐพ๊ธฐ + item.media + .iter() + .find(|m| m.content.iter().any(|c| c.medium.as_deref() == Some("image"))) + .and_then(|m| m.content.first()) + .and_then(|c| c.url.clone()) + .map(|url| url.to_string()) +} + pub fn get_field_value( item: &Entry, config: &RssConfig, @@ -38,8 +66,6 @@ pub fn get_field_value( }; if let Some(_p) = path { - // Path-based resolution is harder with feed-rs because it's already abstracted. - // We'll map some known paths to feed-rs fields. match _p.as_str() { "title" => item.title.as_ref().map(|t| t.content.clone()), "link" => item.links.first().map(|l| l.href.clone()), @@ -93,9 +119,15 @@ pub fn build_content(config: &RssConfig, item: &Entry) -> String { parts.push(format!("\n{}\n", markdown.trim())); } - if let Some(pd) = pub_date { - parts.push(format!("\n-# ๐Ÿ• ", pd.timestamp())); - } + let timestamp = pub_date + .map(|pd| pd.timestamp()) + .unwrap_or_else(|| { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64 + }); + parts.push(format!("\n-# ๐Ÿ• ", timestamp)); let joined = parts.join("\n"); if joined.is_empty() {