From 830e9c7eb91aeaefb8fcae6dc778bc8d1f2c0ffb Mon Sep 17 00:00:00 2001 From: imnyang Date: Fri, 9 Jan 2026 08:46:44 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EC=A0=81=20rss.toml=EA=B3=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20rss=EB=A5=BC=20=EC=9E=98?= =?UTF-8?q?=20=EC=9D=B4=EC=81=98=EA=B2=8C=20=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +----- rss.toml | 17 +++++- src/lib/rss.ts | 128 ++++++++++++++++++++++++++++++++++++--------- src/type/toml.d.ts | 10 +--- src/type/types.ts | 9 +++- 5 files changed, 131 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 3929bdd..fcb9a14 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,3 @@ # memos-rss -제 [Memos](https://discord.gg/J3XfJ8tZRj) 디스코드 서버의 RSS 피드 알림 봇입니다. - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run start -``` - -This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +제 [Memos](https://discord.gg/J3XfJ8tZRj) 디스코드 서버의 RSS 피드 알림 봇입니다. \ No newline at end of file diff --git a/rss.toml b/rss.toml index 1f8918f..e4c96f0 100644 --- a/rss.toml +++ b/rss.toml @@ -4,9 +4,24 @@ tag = "1448338238963056691" emoji = "🤓" rss = "https://feeds.feedburner.com/geeknews-feed" +[geeknews.setup] +title = "title" +link = "link" +content = "content" +author = "author.name" +authorLink = "author.uri" +pubDate = "published" + [svrforum] channel = "1448338163864174867" tag = "1448338289147908108" emoji = "💻" rss = "https://svrforum.com/rss" -category_filter = ["유머&정보"] \ No newline at end of file +category_filter = ["유머&정보"] + +[svrforum.setup] +title = "title" +link = "link" +content = "description" +author = "dc:creator" +pubDate = "pubDate" diff --git a/src/lib/rss.ts b/src/lib/rss.ts index de89e40..34eb6dc 100644 --- a/src/lib/rss.ts +++ b/src/lib/rss.ts @@ -4,14 +4,21 @@ import TurndownService from "turndown"; import { parser } from "./config"; import { processedItems, saveCurrentState } from "./storage"; import type { RssSource } from "../type/types"; -import rssConfig from "@/../rss.toml"; - -console.log("Loaded RSS Config:", JSON.stringify(rssConfig, null, 2)); export async function checkRssFeeds(client: Client): Promise { console.log(`[${new Date().toISOString()}] Checking RSS feeds...`); - for (const [name, config] of Object.entries(rssConfig) as [string, RssSource][]) { + let rssConfig: Record; + try { + const configFile = Bun.file("./rss.toml"); + const content = await configFile.text(); + rssConfig = Bun.TOML.parse(content) as Record; + } catch (error) { + console.error("Failed to load rss.toml:", error); + return; + } + + for (const [name, config] of Object.entries(rssConfig)) { try { await processRssFeed(name, config, client); } catch (error) { @@ -20,6 +27,44 @@ export async function checkRssFeeds(client: Client): Promise { } } +function getValueByPath(obj: any, path: string | undefined): any { + if (!path) return undefined; + if (obj[path] !== undefined) return obj[path]; + return path.split(".").reduce((acc, part) => acc && acc[part], obj); +} + +function getFieldValue( + item: Parser.Item, + config: RssSource, + field: keyof NonNullable +): any { + const path = config.setup?.[field]; + const itemany = item as any; + if (path) { + return getValueByPath(itemany, path); + } + + switch (field) { + case "title": + return item.title; + case "link": + return item.link; + case "content": + return item.content || item.contentSnippet; + case "pubDate": + return item.pubDate; + case "author": + return ( + itemany.creator || + (typeof itemany.author === "string" + ? itemany.author + : itemany.author?.name) + ); + default: + return undefined; + } +} + async function processRssFeed( name: string, config: RssSource, @@ -35,7 +80,9 @@ async function processRssFeed( // 현재 RSS 피드에 있는 항목들의 ID 수집 const currentFeedIds = new Set( - feed.items.map((item) => item.link || item.guid || "").filter(Boolean) + feed.items + .map((item) => getFieldValue(item, config, "link") || item.guid || "") + .filter(Boolean) ); // RSS 피드에서 사라진 항목들을 처리된 목록에서 제거 @@ -50,25 +97,31 @@ async function processRssFeed( for (const itemId of removedItems) { processed.delete(itemId); } - console.log(`[${name}] Cleaned up ${removedItems.length} old items from processed list`); + console.log( + `[${name}] Cleaned up ${removedItems.length} old items from processed list` + ); saveCurrentState(); } for (const item of feed.items) { - const itemId = item.link || item.guid || ""; + const itemId = getFieldValue(item, config, "link") || item.guid || ""; if (!itemId || processed.has(itemId)) continue; // 카테고리 필터링 if (shouldFilterByCategory(config, item)) { + const title = getFieldValue(item, config, "title") || "Untitled"; console.log( - `[${name}] Filtered by category: ${item.title} (${item.categories?.join(", ")})` + `[${name}] Filtered by category: ${title} (${item.categories?.join( + ", " + )})` ); processed.add(itemId); continue; } - console.log(`[${name}] New item found: ${item.title}`); + const title = getFieldValue(item, config, "title") || "Untitled"; + console.log(`[${name}] New item found: ${title}`); await postToForum(config, item, client); processed.add(itemId); @@ -89,16 +142,21 @@ async function postToForum( client: Client ): Promise { const startTime = performance.now(); - + try { const channel = await client.channels.fetch(config.channel); if (!channel || !(channel instanceof ForumChannel)) { - console.error(`Channel ${config.channel} not found or is not a forum channel.`); + console.error( + `Channel ${config.channel} not found or is not a forum channel.` + ); return; } - const title = (item.title || "Untitled").slice(0, 100); + const title = (getFieldValue(item, config, "title") || "Untitled").slice( + 0, + 100 + ); const content = buildContent(config, item); await channel.threads.create({ @@ -109,10 +167,15 @@ async function postToForum( }); const endTime = performance.now(); - console.log(`[Forum] Post completed: ${title} (${(endTime - startTime).toFixed(2)}ms)`); + console.log( + `[Forum] Post completed: ${title} (${(endTime - startTime).toFixed(2)}ms)` + ); } catch (error) { const endTime = performance.now(); - console.error(`[Forum] Post failed (${(endTime - startTime).toFixed(2)}ms):`, error); + console.error( + `[Forum] Post failed (${(endTime - startTime).toFixed(2)}ms):`, + error + ); } } @@ -120,21 +183,38 @@ function buildContent(config: RssSource, item: Parser.Item): string { const parts: string[] = []; const turndownService = new TurndownService(); - if (item.link) { - parts.push(`# ${config.emoji} | [${item.title}](<${item.link}>)`); + const title = getFieldValue(item, config, "title") || "Untitled"; + const link = getFieldValue(item, config, "link"); + const content = getFieldValue(item, config, "content"); + const author = getFieldValue(item, config, "author"); + const authorLink = getFieldValue(item, config, "authorLink"); + const pubDate = getFieldValue(item, config, "pubDate"); + + if (link) { + parts.push(`# ${config.emoji} | [${title}](<${link}>)`); + } else { + parts.push(`# ${config.emoji} | ${title}`); } - if (item.content) { - const markdown = turndownService.turndown(item.content); + if (author) { + if (authorLink) { + parts.push(`-# 🖊️ [${author}](<${authorLink}>)`); + } else { + parts.push(`-# 🖊️ ${author}`); + } + } + + if (content) { + const markdown = turndownService.turndown(content); parts.push(`\n${markdown.trim()}\n`); - } else if (item.contentSnippet) { - parts.push(`\n${item.contentSnippet}\n`); } - if (item.pubDate) { - const timestamp = Math.floor(new Date(item.pubDate).getTime() / 1000); - parts.push(`\n-# 🕐 `); + if (pubDate) { + const timestamp = Math.floor(new Date(pubDate).getTime() / 1000); + if (!isNaN(timestamp)) { + parts.push(`\n-# 🕐 `); + } } - return parts.join("\n") || item.link || "No content available."; + return parts.join("\n") || link || "No content available."; } diff --git a/src/type/toml.d.ts b/src/type/toml.d.ts index 00ded64..a3682e8 100644 --- a/src/type/toml.d.ts +++ b/src/type/toml.d.ts @@ -1,11 +1,5 @@ declare module "*.toml" { - const value: Record; + import { RssSource } from "./types"; + const value: Record; export default value; } diff --git a/src/type/types.ts b/src/type/types.ts index bd6837f..b0ae8e8 100644 --- a/src/type/types.ts +++ b/src/type/types.ts @@ -2,7 +2,14 @@ export interface RssSource { channel: string; tag: string; emoji: string; - body: string; rss: string; category_filter?: string[]; + setup?: { + title?: string; + link?: string; + content?: string; + author?: string; + authorLink?: string; + pubDate?: string; + }; }