동적 rss.toml과 설정에서 rss를 잘 이쁘게 할 수 있도록 수정
This commit is contained in:
parent
91594448df
commit
830e9c7eb9
5 changed files with 131 additions and 49 deletions
16
README.md
16
README.md
|
|
@ -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.
|
|
||||||
17
rss.toml
17
rss.toml
|
|
@ -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"
|
||||||
|
|
|
||||||
128
src/lib/rss.ts
128
src/lib/rss.ts
|
|
@ -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);
|
||||||
|
|
@ -89,16 +142,21 @@ async function postToForum(
|
||||||
client: Client
|
client: Client
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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);
|
||||||
parts.push(`\n-# 🕐 <t:${timestamp}:f>`);
|
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
10
src/type/toml.d.ts
vendored
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue