import { TWITTER_API, TWITTER_SEARCH_QUERIES, TWITTER_MAX_AGE_HOURS, TWITTER_MAX_RESULTS, TWITTER_SNIPPET_LENGTH, } from '../config'; import { log } from '../utils'; // --- Types --- interface TweetMetrics { like_count: number; retweet_count: number; reply_count: number; impression_count: number; } interface Tweet { id: string; text: string; author_id: string; public_metrics: TweetMetrics; created_at: string; } interface TwitterUser { id: string; name: string; username: string; } interface TwitterSearchResponse { data?: Tweet[]; includes?: { users?: TwitterUser[] }; meta?: { result_count: number; next_token?: string }; } // --- API --- function getToken(): string { const token = process.env.TWITTER_BEARER_TOKEN; if (!token) { throw new Error( 'TWITTER_BEARER_TOKEN not set in environment' ); } return token; } function buildStartTime(): string { const d = new Date(); d.setHours(d.getHours() - TWITTER_MAX_AGE_HOURS); return d.toISOString(); } async function searchTweets( query: string ): Promise { const params = new URLSearchParams({ query, max_results: '100', 'tweet.fields': 'public_metrics,created_at,author_id', expansions: 'author_id', 'user.fields': 'name,username', start_time: buildStartTime(), }); const url = `${TWITTER_API}/tweets/search/recent?${params}`; log(` fetching: ${query.slice(0, 60)}...`); const res = await fetch(url, { headers: { Authorization: `Bearer ${getToken()}` }, }); if (!res.ok) { const body = await res.text(); log(` ⚠ Twitter API ${res.status}: ${body}`); return {}; } return (await res.json()) as TwitterSearchResponse; } // --- Filter & Sort --- function engagementScore(t: Tweet): number { const m = t.public_metrics; return m.like_count + m.retweet_count * 2; } function topTweets(tweets: Tweet[]): Tweet[] { return [...tweets] .sort((a, b) => engagementScore(b) - engagementScore(a)) .slice(0, TWITTER_MAX_RESULTS); } // --- Format --- function snippet(text: string): string { const clean = text.replace(/\n+/g, ' ').trim(); if (clean.length <= TWITTER_SNIPPET_LENGTH) return clean; return clean.slice(0, TWITTER_SNIPPET_LENGTH).trimEnd() + '…'; } function tweetUrl(username: string, id: string): string { return `https://x.com/${username}/status/${id}`; } function formatTweet( tweet: Tweet, users: Map ): string { const user = users.get(tweet.author_id); const name = user?.name ?? 'Unknown'; const handle = user?.username ?? 'unknown'; const m = tweet.public_metrics; const link = tweetUrl(handle, tweet.id); return ( `**${name}** ([@${handle}](${link}))\n` + `> ${snippet(tweet.text)}\n` + `❤️ ${m.like_count.toLocaleString()} | ` + `🔁 ${m.retweet_count.toLocaleString()}` ); } function formatDigest( tweets: Tweet[], users: Map ): string { if (!tweets.length) { return '*No trending AI tweets found in the last 24h.*'; } const body = tweets.map((t) => formatTweet(t, users)).join('\n\n'); const now = new Date().toISOString().slice(0, 16).replace('T', ' '); return `# 🐦 Trending AI Tweets — ${now} UTC\n\n${body}\n`; } // --- Entry --- export async function run(): Promise { log('[twitter] Searching for trending AI tweets...'); const allTweets: Tweet[] = []; const userMap = new Map(); for (const query of TWITTER_SEARCH_QUERIES) { const resp = await searchTweets(query); if (resp.data) allTweets.push(...resp.data); if (resp.includes?.users) { for (const u of resp.includes.users) { userMap.set(u.id, u); } } log(` ${resp.meta?.result_count ?? 0} results`); } // Dedupe by tweet id const seen = new Set(); const unique = allTweets.filter((t) => { if (seen.has(t.id)) return false; seen.add(t.id); return true; }); const top = topTweets(unique); log(`[twitter] ${unique.length} total, ${top.length} top tweets`); return formatDigest(top, userMap); } if (import.meta.main) { console.log(await run()); }