add rssapp
This commit is contained in:
parent
640e9ee892
commit
8a7baefd76
20
services/rssapp/.dockerignore
Normal file
20
services/rssapp/.dockerignore
Normal file
@ -0,0 +1,20 @@
|
||||
# .dockerignore
|
||||
# Generated by Data Craftsman Tools
|
||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output
|
||||
coverage/
|
||||
dist/
|
||||
build/
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
144
services/rssapp/.gitignore
vendored
Normal file
144
services/rssapp/.gitignore
vendored
Normal file
@ -0,0 +1,144 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
23
services/rssapp/Dockerfile
Normal file
23
services/rssapp/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
# --- Base image
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# --- Install dependencies
|
||||
FROM base AS deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# --- Build / final image
|
||||
FROM base AS runner
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "index.js"]
|
148
services/rssapp/README.md
Normal file
148
services/rssapp/README.md
Normal file
@ -0,0 +1,148 @@
|
||||
# rssapp
|
||||
|
||||
A HTTP service which dynamically generates RSS feeds for recent lewdtuber tweets (X Posts). Tweet data is cached for 30 mins.
|
||||
|
||||
## Example usage
|
||||
|
||||
GET `https://rssapp.example.com/projektmelody`
|
||||
|
||||
### Example response
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||
<channel>
|
||||
<title><![CDATA[projektmelody Tweets]]></title>
|
||||
<description><![CDATA[Latest tweets from projektmelody]]></description>
|
||||
<link>https://x.com/projektmelody</link>
|
||||
<generator>RSS for Node</generator>
|
||||
<lastBuildDate>Mon, 25 Aug 2025 14:50:15 GMT</lastBuildDate>
|
||||
<atom:link href="https://rssapp.sbtp.xyz/projektmelody.xml" rel="self" type="application/rss+xml"/>
|
||||
<language><![CDATA[en]]></language>
|
||||
<item>
|
||||
<title><![CDATA[Did I mention this exists?? 🎶]]></title>
|
||||
<description><![CDATA[Did I mention this exists?? 🎶]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959854383627489458</link>
|
||||
<guid isPermaLink="false">1959854383627489458</guid>
|
||||
<pubDate>Mon, 25 Aug 2025 05:44:32 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[I'M LIVE!
|
||||
|
||||
FANSLY: https://t.co/mh1izesG2p
|
||||
OF: https://t.co/PhUjaazNrn
|
||||
CB: https://t.co/cxNMAItlt2]]></title>
|
||||
<description><![CDATA[I'M LIVE!
|
||||
|
||||
FANSLY: https://t.co/mh1izesG2p
|
||||
OF: https://t.co/PhUjaazNrn
|
||||
CB: https://t.co/cxNMAItlt2]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959774984387416463</link>
|
||||
<guid isPermaLink="false">1959774984387416463</guid>
|
||||
<pubDate>Mon, 25 Aug 2025 00:29:01 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[runnning 10 min late sry!!!!]]></title>
|
||||
<description><![CDATA[runnning 10 min late sry!!!!]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959766931898249709</link>
|
||||
<guid isPermaLink="false">1959766931898249709</guid>
|
||||
<pubDate>Sun, 24 Aug 2025 23:57:01 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[BRUH I HAVE A SCHECHULE!!!!!!!! https://t.co/jXwtWlozwJ]]></title>
|
||||
<description><![CDATA[BRUH I HAVE A SCHECHULE!!!!!!!! https://t.co/jXwtWlozwJ]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959428737021407540</link>
|
||||
<guid isPermaLink="false">1959428737021407540</guid>
|
||||
<pubDate>Sun, 24 Aug 2025 01:33:09 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[@RemixKun i h8 it here]]></title>
|
||||
<description><![CDATA[@RemixKun i h8 it here]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959185583223550227</link>
|
||||
<guid isPermaLink="false">1959185583223550227</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 09:26:57 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[At least it isn't inflation https://t.co/jgCU1Ouy3e]]></title>
|
||||
<description><![CDATA[At least it isn't inflation https://t.co/jgCU1Ouy3e]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959184510421004782</link>
|
||||
<guid isPermaLink="false">1959184510421004782</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 09:22:41 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[🦋⁺. ✦ NEW COLLAB SONG DROPPED!!! ✦ . ⁺ 🦋
|
||||
|
||||
"SNØWCRASH N' BURN" (track 2) is so pretty! Hard not to ]]></title>
|
||||
<description><![CDATA[🦋⁺. ✦ NEW COLLAB SONG DROPPED!!! ✦ . ⁺ 🦋
|
||||
|
||||
"SNØWCRASH N' BURN" (track 2) is so pretty! Hard not to get lost in the drum beats & synth waves. My chest is full of little virtual butterflies.
|
||||
|
||||
Cynthoni sure puts out bangers.
|
||||
|
||||
LISTEN HERE: https://t.co/VEoQgLc269 https://t.co/e9YU1vSTPi]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959100103350001770</link>
|
||||
<guid isPermaLink="false">1959100103350001770</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 03:47:17 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[@brittblusierra なんだ??]]></title>
|
||||
<description><![CDATA[@brittblusierra なんだ??]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959069147528405036</link>
|
||||
<guid isPermaLink="false">1959069147528405036</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 01:44:17 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[@RemixKun yes, totally not wearing a push-up bra stuffed with cantalope]]></title>
|
||||
<description><![CDATA[@RemixKun yes, totally not wearing a push-up bra stuffed with cantalope]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959066423764165007</link>
|
||||
<guid isPermaLink="false">1959066423764165007</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 01:33:27 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[@ArmadaJones ill make it fit >.>]]></title>
|
||||
<description><![CDATA[@ArmadaJones ill make it fit >.>]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959066077096550732</link>
|
||||
<guid isPermaLink="false">1959066077096550732</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 01:32:05 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[STREAM TOMORROW EVENING!~
|
||||
I'm gonna give so many hugs!
|
||||
|
||||
Wait... I'm in the computer. O.o
|
||||
|
||||
Ok, I'll h]]></title>
|
||||
<description><![CDATA[STREAM TOMORROW EVENING!~
|
||||
I'm gonna give so many hugs!
|
||||
|
||||
Wait... I'm in the computer. O.o
|
||||
|
||||
Ok, I'll have to use my mouth.
|
||||
Mouth hugs for everyone! https://t.co/hidiMBkc2H]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1959065532155797966</link>
|
||||
<guid isPermaLink="false">1959065532155797966</guid>
|
||||
<pubDate>Sat, 23 Aug 2025 01:29:55 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[.✧˚️RAWR! I'VE RETURNED!˚️✧.
|
||||
|
||||
My chakras are aligned. Goals are clear.
|
||||
& I'm ready for a new ch]]></title>
|
||||
<description><![CDATA[.✧˚️RAWR! I'VE RETURNED!˚️✧.
|
||||
|
||||
My chakras are aligned. Goals are clear.
|
||||
& I'm ready for a new chapter.
|
||||
|
||||
I think i just needed a break tbh... but im back. And missed the balls outta y'all! (TᴖT)
|
||||
|
||||
STREAM RESUMES➡️Aug 23 @ 7 pm CST
|
||||
(Fansly/CB/OF)
|
||||
|
||||
See yall soon, space cowboys!]]></description>
|
||||
<link>https://x.com/ProjektMelody/status/1958897664072065467</link>
|
||||
<guid isPermaLink="false">1958897664072065467</guid>
|
||||
<pubDate>Fri, 22 Aug 2025 14:22:52 GMT</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
```
|
||||
|
17
services/rssapp/env.js
Normal file
17
services/rssapp/env.js
Normal file
@ -0,0 +1,17 @@
|
||||
import '@dotenvx/dotenvx/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const EnvSchema = z.object({
|
||||
APIFY_TOKEN: z.string().min(1, "APIFY_TOKEN is required"),
|
||||
ORIGIN: z.string().min(1, "ORIGIN is required"),
|
||||
WHITELIST: z.string().min(1, "WHITELIST is required"), // raw comma-separated string
|
||||
});
|
||||
|
||||
// Parse raw env first
|
||||
const parsed = EnvSchema.parse(process.env);
|
||||
|
||||
// Return env object with WHITELIST converted to Set
|
||||
export const env = {
|
||||
...parsed,
|
||||
WHITELIST: new Set(parsed.WHITELIST.split(',').map(u => u.trim().toLowerCase())),
|
||||
};
|
116
services/rssapp/index.js
Normal file
116
services/rssapp/index.js
Normal file
@ -0,0 +1,116 @@
|
||||
// index.js
|
||||
import Fastify from "fastify";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import RSS from "rss";
|
||||
import TTLCache from '@isaacs/ttlcache';
|
||||
import '@dotenvx/dotenvx/config.js';
|
||||
import { ApifyClient } from 'apify-client';
|
||||
import { env } from './env.js';
|
||||
|
||||
const fastify = Fastify({ logger: true });
|
||||
|
||||
|
||||
const client = new ApifyClient({
|
||||
token: env.APIFY_TOKEN,
|
||||
});
|
||||
|
||||
const cache = new TTLCache({
|
||||
max: 5000,
|
||||
ttl: 30 * 60 * 1000, // 30 minutes
|
||||
});
|
||||
|
||||
// Format date as YYYY-MM-DD_HH:mm:ss_UTC
|
||||
function formatTwitterDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const year = date.getUTCFullYear();
|
||||
const month = pad(date.getUTCMonth() + 1);
|
||||
const day = pad(date.getUTCDate());
|
||||
const hours = pad(date.getUTCHours());
|
||||
const minutes = pad(date.getUTCMinutes());
|
||||
const seconds = pad(date.getUTCSeconds());
|
||||
return `${year}-${month}-${day}_${hours}:${minutes}:${seconds}_UTC`;
|
||||
}
|
||||
|
||||
async function getJson(username) {
|
||||
// 15 days ago
|
||||
const sinceDate = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000);
|
||||
const formatted = formatTwitterDate(sinceDate);
|
||||
|
||||
const { defaultDatasetId } = await client.actor('kaitoeasyapi/twitter-x-data-tweet-scraper-pay-per-result-cheapest').call({
|
||||
searchTerms: [`from:${username} since:${formatted}`]
|
||||
});
|
||||
|
||||
// Fetches results from the actor's dataset.
|
||||
const { items } = await client.dataset(defaultDatasetId).listItems();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
// Helper to build RSS feed
|
||||
function buildFeed(username, tweets) {
|
||||
const feed = new RSS({
|
||||
title: `${username} Tweets`,
|
||||
description: `Latest tweets from ${username}`,
|
||||
feed_url: `https://${env.ORIGIN}/${username}`,
|
||||
site_url: `https://x.com/${username}`,
|
||||
language: "en",
|
||||
});
|
||||
|
||||
tweets.forEach((tweet) => {
|
||||
if (tweet.id === -1) return; // skip mock API filler
|
||||
feed.item({
|
||||
title: tweet.text.substring(0, 100),
|
||||
description: tweet.text,
|
||||
url: tweet.url,
|
||||
guid: tweet.id.toString(),
|
||||
date: new Date(tweet.createdAt),
|
||||
});
|
||||
});
|
||||
|
||||
return feed.xml({ indent: true });
|
||||
}
|
||||
|
||||
fastify.get("/:username", async (request, reply) => {
|
||||
const { username } = request.params;
|
||||
|
||||
if (!env.WHITELIST.has(username.toLowerCase())) {
|
||||
return reply.code(403).send({ error: "Forbidden username" });
|
||||
}
|
||||
|
||||
|
||||
// Serve from cache if exists
|
||||
const cached = cache.get(username);
|
||||
if (cached) {
|
||||
fastify.log.info('sending cached data');
|
||||
fastify.log.info(cached);
|
||||
return reply
|
||||
.header("Content-Type", "application/rss+xml")
|
||||
.send(cached.xml);
|
||||
}
|
||||
|
||||
const data = await getJson(username)
|
||||
|
||||
const xml = buildFeed(username, data);
|
||||
|
||||
// Cache result
|
||||
cache.set(username, { xml, timestamp: Date.now() });
|
||||
|
||||
return reply.header("Content-Type", "application/rss+xml").send(xml);
|
||||
});
|
||||
|
||||
// Start server
|
||||
const start = async () => {
|
||||
try {
|
||||
fastify.listen({
|
||||
port: process.env.PORT || 3000,
|
||||
host: "0.0.0.0"
|
||||
});
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
1519
services/rssapp/package-lock.json
generated
Normal file
1519
services/rssapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
services/rssapp/package.json
Normal file
22
services/rssapp/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "rssapp",
|
||||
"version": "69.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "^1.49.0",
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"apify-client": "^2.15.1",
|
||||
"fastify": "^5.5.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"rss": "^1.2.2",
|
||||
"zod": "^4.1.1"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user