Add support for image uploads and link filtering in RSS feeds

This commit is contained in:
암냥 2026-01-15 06:12:12 +09:00
commit c9f8f8f276
No known key found for this signature in database
9 changed files with 94 additions and 8 deletions

View file

@ -1 +1,3 @@
DISCORD_TOKEN=your_discord_bot_token_here
RSS_USER_AGENT=your_custom_user_agent_here
INTERVAL_MINUTES=5

1
Cargo.lock generated
View file

@ -1033,6 +1033,7 @@ dependencies = [
"dotenvy",
"feed-rs",
"htmd",
"regex",
"reqwest",
"serde",
"serde_json",

View file

@ -16,3 +16,4 @@ anyhow = "1.0"
chrono = "0.4"
sled = "0.34"
dotenvy = "0.15"
regex = "1"

View file

@ -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`.

View file

@ -7,3 +7,4 @@ services:
volumes:
- ./data:/usr/app/data
- ./rss.toml:/usr/app/rss.toml
- /etc/localtime:/etc/localtime:ro

View file

@ -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"

View file

@ -8,6 +8,9 @@ pub struct RssConfig {
pub emoji: String,
pub rss: String,
pub category_filter: Option<Vec<String>>,
pub link_filter: Option<Vec<String>>,
#[serde(default)]
pub upload_image: bool,
pub setup: Option<SetupConfig>,
}

View file

@ -77,6 +77,16 @@ async fn main() -> Result<()> {
Ok(())
}
async fn download_image(url: &str) -> Result<Vec<u8>> {
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::<u64>()?;
// 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 {

View file

@ -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<Feed> {
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<Feed> {
}
}
pub fn should_include_by_link(link: Option<&String>, link_filters: Option<&Vec<String>>) -> 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<String> {
// 첫 번째로 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-# 🕐 <t:{}:f>", 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-# 🕐 <t:{}:f>", timestamp));
let joined = parts.join("\n");
if joined.is_empty() {