initial commit

This commit is contained in:
Eugene Shulga 2025-05-15 11:47:32 +02:00
commit d16c0c836f
8 changed files with 232 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --production
COPY . .
EXPOSE 3000
CMD ["bun", "index.js"]

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# rss-tools
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run
```
This project was created using `bun init` in bun v1.2.8. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

71
app.ts Normal file
View file

@ -0,0 +1,71 @@
import { Hono } from 'hono';
import { serve } from 'bun';
import Parser from 'rss-parser';
import { Feed, type Item } from 'feed';
const app = new Hono();
const parser = new Parser();
app.get('/group-by-day', async (c) => {
const feedUrl = c.req.query('feedUrl');
if (!feedUrl) {
return c.text('Missing feedUrl query parameter', 400);
}
try {
const { title, feedUrl: feedId, link: feedLink, items } = await parser.parseURL(feedUrl);
const grouped = items.reduce((acc, { pubDate, content, contentSnippet, summary, link }) => {
const day = new Date(pubDate as string).toISOString().slice(0, 10);
acc[day] = acc[day] || [];
acc[day].push({
link,
content: content || contentSnippet || summary || ''
});
return acc;
}, {});
const days = Object.keys(grouped).sort().reverse();
const today = new Date();
today.setHours(0, 0, 0, 0);
const feedItems: Item[] = days.map(day => {
const dateStr = new Date(day).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
return {
title: `${title} - ${dateStr}`,
id: day,
date: new Date(day),
link: grouped[day][0].link,
content: grouped[day].map((p: any) => p.content).join('\n\n---------------------\n\n')
};
}).filter(p => p.date < today);
const outFeed = new Feed({
title: title as string,
id: feedId as string,
link: feedLink,
copyright: ''
});
for (const item of feedItems) {
outFeed.addItem(item)
}
const xml = outFeed.rss2();
return c.text(xml, 200, {
'Content-Type': 'text/xml',
});
} catch (err: any) {
return c.text(`Error fetching or processing feed: ${err.message}`, 500);
}
});
// Start Bun server on port 3000
serve({
fetch: app.fetch,
port: 3000
});

46
bun.lock Normal file
View file

@ -0,0 +1,46 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "rss-tools",
"dependencies": {
"feed": "^5.0.1",
"hono": "^4.7.9",
"rss-parser": "^3.13.0",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
"entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"feed": ["feed@5.0.1", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-kOveKLHucVZ6RI91bdWAts9O0L1N2mGzRGVCDQPRnHh4HmgqLdN66Cp/5dMeJZGn/qnBslWliwX37FEBq8DCIA=="],
"hono": ["hono@4.7.9", "", {}, "sha512-/EsCoR5h7N4yu01TDu9GMCCJa6ZLk5ZJIWFFGNawAXmd1Tp53+Wir4xm0D2X19bbykWUlzQG0+BvPAji6p9E8Q=="],
"rss-parser": ["rss-parser@3.13.0", "", { "dependencies": { "entities": "^2.0.3", "xml2js": "^0.5.0" } }, "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w=="],
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
}
}

11
docker-compose.yml Normal file
View file

@ -0,0 +1,11 @@
services:
app:
build: .
env_file:
- .env
environment:
- PORT=${PORT:-3000}
ports:
- "${PORT:-3000}:${PORT:-3000}"
restart: unless-stopped

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "rss-tools",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"feed": "^5.0.1",
"hono": "^4.7.9",
"rss-parser": "^3.13.0"
}
}

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}