// 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();