동적 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](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 피드 알림 봇입니다.

View file

@ -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 = ["유머&정보"]
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 { 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<void> {
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 {
await processRssFeed(name, config, client);
} 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(
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<void> {
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-# 🕐 <t:${timestamp}:f>`);
if (pubDate) {
const timestamp = Math.floor(new Date(pubDate).getTime() / 1000);
if (!isNaN(timestamp)) {
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" {
const value: Record<string, {
channel: string;
tag: string;
emoji: string;
body: string;
rss: string;
category_filter?: string[];
}>;
import { RssSource } from "./types";
const value: Record<string, RssSource>;
export default value;
}

View file

@ -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;
};
}