동적 rss.toml과 설정에서 rss를 잘 이쁘게 할 수 있도록 수정

This commit is contained in:
암냥 2026-01-09 08:46:44 +09:00
commit 830e9c7eb9
No known key found for this signature in database
5 changed files with 131 additions and 49 deletions

View file

@ -1,17 +1,3 @@
# memos-rss # memos-rss
제 [Memos](https://discord.gg/J3XfJ8tZRj) 디스코드 서버의 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.

View file

@ -4,9 +4,24 @@ tag = "1448338238963056691"
emoji = "🤓" emoji = "🤓"
rss = "https://feeds.feedburner.com/geeknews-feed" rss = "https://feeds.feedburner.com/geeknews-feed"
[geeknews.setup]
title = "title"
link = "link"
content = "content"
author = "author.name"
authorLink = "author.uri"
pubDate = "published"
[svrforum] [svrforum]
channel = "1448338163864174867" channel = "1448338163864174867"
tag = "1448338289147908108" tag = "1448338289147908108"
emoji = "💻" emoji = "💻"
rss = "https://svrforum.com/rss" rss = "https://svrforum.com/rss"
category_filter = ["유머&정보"] category_filter = ["유머&정보"]
[svrforum.setup]
title = "title"
link = "link"
content = "description"
author = "dc:creator"
pubDate = "pubDate"

View file

@ -4,14 +4,21 @@ import TurndownService from "turndown";
import { parser } from "./config"; import { parser } from "./config";
import { processedItems, saveCurrentState } from "./storage"; import { processedItems, saveCurrentState } from "./storage";
import type { RssSource } from "../type/types"; 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<void> { export async function checkRssFeeds(client: Client): Promise<void> {
console.log(`[${new Date().toISOString()}] Checking RSS feeds...`); console.log(`[${new Date().toISOString()}] Checking RSS feeds...`);
for (const [name, config] of Object.entries(rssConfig) as [string, RssSource][]) { let rssConfig: Record<string, RssSource>;
try {
const configFile = Bun.file("./rss.toml");
const content = await configFile.text();
rssConfig = Bun.TOML.parse(content) as Record<string, RssSource>;
} catch (error) {
console.error("Failed to load rss.toml:", error);
return;
}
for (const [name, config] of Object.entries(rssConfig)) {
try { try {
await processRssFeed(name, config, client); await processRssFeed(name, config, client);
} catch (error) { } catch (error) {
@ -20,6 +27,44 @@ export async function checkRssFeeds(client: Client): Promise<void> {
} }
} }
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<RssSource["setup"]>
): 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( async function processRssFeed(
name: string, name: string,
config: RssSource, config: RssSource,
@ -35,7 +80,9 @@ async function processRssFeed(
// 현재 RSS 피드에 있는 항목들의 ID 수집 // 현재 RSS 피드에 있는 항목들의 ID 수집
const currentFeedIds = new Set( 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 피드에서 사라진 항목들을 처리된 목록에서 제거 // RSS 피드에서 사라진 항목들을 처리된 목록에서 제거
@ -50,25 +97,31 @@ async function processRssFeed(
for (const itemId of removedItems) { for (const itemId of removedItems) {
processed.delete(itemId); 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(); saveCurrentState();
} }
for (const item of feed.items) { 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 (!itemId || processed.has(itemId)) continue;
// 카테고리 필터링 // 카테고리 필터링
if (shouldFilterByCategory(config, item)) { if (shouldFilterByCategory(config, item)) {
const title = getFieldValue(item, config, "title") || "Untitled";
console.log( console.log(
`[${name}] Filtered by category: ${item.title} (${item.categories?.join(", ")})` `[${name}] Filtered by category: ${title} (${item.categories?.join(
", "
)})`
); );
processed.add(itemId); processed.add(itemId);
continue; 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); await postToForum(config, item, client);
processed.add(itemId); processed.add(itemId);
@ -94,11 +147,16 @@ async function postToForum(
const channel = await client.channels.fetch(config.channel); const channel = await client.channels.fetch(config.channel);
if (!channel || !(channel instanceof ForumChannel)) { 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; return;
} }
const title = (item.title || "Untitled").slice(0, 100); const title = (getFieldValue(item, config, "title") || "Untitled").slice(
0,
100
);
const content = buildContent(config, item); const content = buildContent(config, item);
await channel.threads.create({ await channel.threads.create({
@ -109,10 +167,15 @@ async function postToForum(
}); });
const endTime = performance.now(); 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) { } catch (error) {
const endTime = performance.now(); 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 parts: string[] = [];
const turndownService = new TurndownService(); const turndownService = new TurndownService();
if (item.link) { const title = getFieldValue(item, config, "title") || "Untitled";
parts.push(`# ${config.emoji} | [${item.title}](<${item.link}>)`); 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) { if (author) {
const markdown = turndownService.turndown(item.content); if (authorLink) {
parts.push(`-# 🖊️ [${author}](<${authorLink}>)`);
} else {
parts.push(`-# 🖊️ ${author}`);
}
}
if (content) {
const markdown = turndownService.turndown(content);
parts.push(`\n${markdown.trim()}\n`); parts.push(`\n${markdown.trim()}\n`);
} else if (item.contentSnippet) {
parts.push(`\n${item.contentSnippet}\n`);
} }
if (item.pubDate) { if (pubDate) {
const timestamp = Math.floor(new Date(item.pubDate).getTime() / 1000); const timestamp = Math.floor(new Date(pubDate).getTime() / 1000);
if (!isNaN(timestamp)) {
parts.push(`\n-# 🕐 <t:${timestamp}:f>`); parts.push(`\n-# 🕐 <t:${timestamp}:f>`);
} }
}
return parts.join("\n") || item.link || "No content available."; return parts.join("\n") || link || "No content available.";
} }

10
src/type/toml.d.ts vendored
View file

@ -1,11 +1,5 @@
declare module "*.toml" { declare module "*.toml" {
const value: Record<string, { import { RssSource } from "./types";
channel: string; const value: Record<string, RssSource>;
tag: string;
emoji: string;
body: string;
rss: string;
category_filter?: string[];
}>;
export default value; export default value;
} }

View file

@ -2,7 +2,14 @@ export interface RssSource {
channel: string; channel: string;
tag: string; tag: string;
emoji: string; emoji: string;
body: string;
rss: string; rss: string;
category_filter?: string[]; category_filter?: string[];
setup?: {
title?: string;
link?: string;
content?: string;
author?: string;
authorLink?: string;
pubDate?: string;
};
} }