144 lines
3.7 KiB
JavaScript
144 lines
3.7 KiB
JavaScript
// index.js
|
|
import Fastify from "fastify";
|
|
import RSS from "rss";
|
|
import TTLCache from '@isaacs/ttlcache';
|
|
import '@dotenvx/dotenvx/config.js';
|
|
import { ApifyClient } from 'apify-client';
|
|
import { env } from './env.js';
|
|
import packageJson from "./package.json" with { type: "json" };
|
|
import cors from '@fastify/cors'
|
|
|
|
const fastify = Fastify({ logger: true });
|
|
fastify.register(cors);
|
|
|
|
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}`],
|
|
maxItems: 15,
|
|
});
|
|
|
|
// 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("/", (request, reply) => {
|
|
return reply
|
|
.code(200)
|
|
.header("Content-Type", "text/html; charset=utf-8")
|
|
.send(`HELLO<br>THIS IS rssapp ${packageJson.version}<br>OK<br>try <a href="/projektmelody">/projektmelody</a>`);
|
|
});
|
|
|
|
|
|
fastify.get("/:username", async (request, reply) => {
|
|
const { username } = request.params;
|
|
const key = username.toLowerCase();
|
|
|
|
if (!key) {
|
|
return reply.code(401).send({ error: "Username missing" });
|
|
}
|
|
|
|
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();
|