Compare commits
No commits in common. "46a9041b9792af00b8d66c664fdfa211f8fad9ae" and "640e9ee89239b0c8b4f95250889ead8c5a99237e" have entirely different histories.
46a9041b97
...
640e9ee892
@ -37,6 +37,8 @@ jobs:
|
|||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.description=The Galaxy's Best VTuber hentai site
|
org.opencontainers.image.description=The Galaxy's Best VTuber hentai site
|
||||||
org.opencontainers.image.title=our
|
org.opencontainers.image.title=our
|
||||||
|
org.opencontainers.image.created={{commit_date 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'}}
|
||||||
|
org.opencontainers.image.version={{version}}
|
||||||
org.opencontainers.image.licenses=unlicense
|
org.opencontainers.image.licenses=unlicense
|
||||||
org.opencontainers.image.source=https://gitea.futureporn.net/futureporn/fp
|
org.opencontainers.image.source=https://gitea.futureporn.net/futureporn/fp
|
||||||
org.opencontainers.image.url=https://gitea.futureporn.net/futureporn/-/packages/container/our
|
org.opencontainers.image.url=https://gitea.futureporn.net/futureporn/-/packages/container/our
|
||||||
|
@ -2,27 +2,24 @@
|
|||||||
#! /usr/local/bin/abs
|
#! /usr/local/bin/abs
|
||||||
|
|
||||||
|
|
||||||
echo(" * Remux the .ts to .mp4 using `ffmpeg -i 2025-08-25T00-12-24Z.ts -c copy projektmelody-chaturbate-2025-08-24.mp4")
|
echo(" * Remux the .ts to .mp4 ")
|
||||||
|
echo(" [Press Enter When Complete...]")
|
||||||
|
_ = stdin()
|
||||||
|
|
||||||
|
|
||||||
|
echo(" * Temporarily serve the mp4 using `npx http-server ./serve` ")
|
||||||
echo(" [Press Enter When Complete...]")
|
echo(" [Press Enter When Complete...]")
|
||||||
_ = stdin()
|
_ = stdin()
|
||||||
|
|
||||||
|
|
||||||
echo(" * Upload the .mp4 to Backblaze ")
|
|
||||||
echo(" * Generate a thumbnail -- bash <(curl -fsSL https://gitea.futureporn.net/futureporn/fp/raw/branch/main/packages/scripts/thumbnail-generator.sh) ./projektmelody-chaturbate-2025-02-12.mp4")
|
echo(" * Generate a thumbnail -- bash <(curl -fsSL https://gitea.futureporn.net/futureporn/fp/raw/branch/main/packages/scripts/thumbnail-generator.sh) ./projektmelody-chaturbate-2025-02-12.mp4")
|
||||||
echo(" * Upload the thumbnail to Backblaze")
|
|
||||||
echo(" [Press Enter When Complete...]")
|
|
||||||
_ = stdin()
|
|
||||||
|
|
||||||
echo(" * Upload the .mp4 to Mux ")
|
echo(" * Upload the .mp4 to Mux ")
|
||||||
|
echo(" * Upload the .mp4 to Backblaze ")
|
||||||
echo(" * Add the .mp4 to IPFS -- sudo -u ipfs ipfs add --cid-version=1 ./projektmelody-chaturbate-2025-02-12.mp4")
|
echo(" * Add the .mp4 to IPFS -- sudo -u ipfs ipfs add --cid-version=1 ./projektmelody-chaturbate-2025-02-12.mp4")
|
||||||
echo(" * Remote pin the IPFS CID")
|
echo(" * Remote pin the IPFS CID")
|
||||||
echo(" [Press Enter When Complete...]")
|
echo(" [Press Enter When Complete...]")
|
||||||
_ = stdin()
|
_ = stdin()
|
||||||
|
|
||||||
echo(" * Create magnet link. ex: `torf ~/Downloads/projektmelody-chaturbate-2025-08-25.mp4 --notorrent --notracker --webseed https://futureporn-b2.b-cdn.net/projektmelody-chaturbate-2025-08-25.mp4'")
|
|
||||||
echo(" [Press Enter When Complete...]")
|
|
||||||
_ = stdin()
|
|
||||||
|
|
||||||
|
|
||||||
echo(" * Create a B2 File (.mp4) in Strapi")
|
echo(" * Create a B2 File (.mp4) in Strapi")
|
||||||
echo(" * Create a B2 File (thumbnail) in Strapi")
|
echo(" * Create a B2 File (thumbnail) in Strapi")
|
||||||
|
@ -3,5 +3,3 @@ ggshield
|
|||||||
click
|
click
|
||||||
ansible
|
ansible
|
||||||
ansible-lint
|
ansible-lint
|
||||||
vcsi
|
|
||||||
torrentfile
|
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# .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
144
services/rssapp/.gitignore
vendored
@ -1,144 +0,0 @@
|
|||||||
# 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
|
|
@ -1,23 +0,0 @@
|
|||||||
# --- 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"]
|
|
@ -1,148 +0,0 @@
|
|||||||
# 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>
|
|
||||||
```
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
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())),
|
|
||||||
};
|
|
@ -1,130 +0,0 @@
|
|||||||
// 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
|
|
||||||
});
|
|
||||||
|
|
||||||
const inflight = new Map();
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
const key = username.toLowerCase();
|
|
||||||
|
|
||||||
if (!env.WHITELIST.has(key)) {
|
|
||||||
return reply.code(403).send({ error: "Forbidden username" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve from cache if exists
|
|
||||||
const cached = cache.get(key);
|
|
||||||
if (cached) {
|
|
||||||
fastify.log.info('sending cached data');
|
|
||||||
return reply.header("Content-Type", "application/rss+xml").send(cached.xml);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataPromise;
|
|
||||||
|
|
||||||
if (inflight.has(key)) {
|
|
||||||
// Wait for the in-progress fetch
|
|
||||||
fastify.log.info('waiting for in-flight fetch');
|
|
||||||
dataPromise = inflight.get(key);
|
|
||||||
} else {
|
|
||||||
// Start a new fetch
|
|
||||||
dataPromise = getJson(key);
|
|
||||||
inflight.set(key, dataPromise);
|
|
||||||
|
|
||||||
// Ensure we remove from inflight map when done
|
|
||||||
dataPromise.finally(() => inflight.delete(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await dataPromise;
|
|
||||||
|
|
||||||
const xml = buildFeed(key, data);
|
|
||||||
|
|
||||||
// Cache result
|
|
||||||
cache.set(key, { 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
1519
services/rssapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"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