Files
Homarr/src/pages/api/modules/rss/index.ts

151 lines
3.7 KiB
TypeScript
Raw Normal View History

import xss from 'xss';
2023-04-05 14:23:52 +09:00
import { NextApiRequest, NextApiResponse } from 'next';
2023-02-15 22:00:06 +01:00
import Consola from 'consola';
import { getCookie } from 'cookies-next';
import { decode, encode } from 'html-entities';
2023-02-15 22:00:06 +01:00
import Parser from 'rss-parser';
import { z } from 'zod';
2023-04-05 14:23:52 +09:00
2023-02-15 22:00:06 +01:00
import { getConfig } from '../../../../tools/config/getConfig';
2023-03-17 22:10:00 +01:00
import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool';
2023-04-05 14:23:52 +09:00
import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
2023-02-15 22:00:06 +01:00
type CustomItem = {
'media:content': string;
enclosure: {
url: string;
};
};
const parser: Parser<any, CustomItem> = new Parser({
customFields: {
item: ['media:content', 'enclosure'],
},
});
const getQuerySchema = z.object({
widgetId: z.string().uuid(),
2023-04-05 14:23:52 +09:00
feedUrl: z.string(),
});
2023-02-15 22:00:06 +01:00
export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
const configName = getCookie('config-name', { req: request });
const config = getConfig(configName?.toString() ?? 'default');
const parseResult = getQuerySchema.safeParse(request.query);
if (!parseResult.success) {
response.status(400).json({ message: 'invalid query parameters, please specify the widgetId' });
return;
}
const rssWidget = config.widgets.find(
(x) => x.type === 'rss' && x.id === parseResult.data.widgetId
) as IRssWidget | undefined;
2023-02-15 22:00:06 +01:00
if (
!rssWidget ||
!rssWidget.properties.rssFeedUrl ||
rssWidget.properties.rssFeedUrl.length < 1
) {
response.status(400).json({ message: 'required widget does not exist' });
return;
}
2023-04-10 23:29:00 +09:00
Consola.info(`Requesting RSS feed at url ${parseResult.data.feedUrl}`);
2023-02-15 22:00:06 +01:00
const stopWatch = new Stopwatch();
2023-04-05 14:23:52 +09:00
const feed = await parser.parseURL(parseResult.data.feedUrl);
2023-02-15 22:00:06 +01:00
Consola.info(`Retrieved RSS feed after ${stopWatch.getEllapsedMilliseconds()} milliseconds`);
const orderedFeed = {
...feed,
items: feed.items
.map((item: { title: string; content: string; 'content:encoded': string }) => ({
2023-02-15 22:00:06 +01:00
...item,
title: item.title ? decode(item.title) : undefined,
content: processItemContent(
item['content:encoded'] ?? item.content,
rssWidget.properties.dangerousAllowSanitizedItemContent
),
2023-02-15 22:00:06 +01:00
enclosure: createEnclosure(item),
2023-02-28 20:35:57 +01:00
link: createLink(item),
2023-02-15 22:00:06 +01:00
}))
.sort((a: { pubDate: number }, b: { pubDate: number }) => {
if (!a.pubDate || !b.pubDate) {
return 0;
}
return a.pubDate - b.pubDate;
})
.slice(0, 20),
};
response.status(200).json({
feed: orderedFeed,
success: orderedFeed?.items !== undefined,
});
};
const processItemContent = (content: string, dangerousAllowSanitizedItemContent: boolean) => {
if (dangerousAllowSanitizedItemContent) {
return xss(content, {
allowList: {
p: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
a: ['href'],
b: [],
strong: [],
i: [],
em: [],
img: ['src', 'width', 'height'],
br: [],
small: [],
ul: [],
li: [],
ol: [],
figure: [],
svg: [],
code: [],
mark: [],
blockquote: [],
},
});
}
return encode(content);
};
2023-02-28 20:35:57 +01:00
const createLink = (item: any) => {
if (item.link) {
return item.link;
}
return item.guid;
};
2023-02-15 22:00:06 +01:00
const createEnclosure = (item: any) => {
if (item.enclosure) {
return item.enclosure;
}
if (item['media:content']) {
return {
url: item['media:content'].$.url,
};
}
return undefined;
};
export default async (request: NextApiRequest, response: NextApiResponse) => {
if (request.method === 'GET') {
return Get(request, response);
}
return response.status(405);
};