import { TwitterApi } from "twitter-api-v2";
import type { Post } from "../types.js";
let twitterClient: TwitterApi | null = null;
export function getTwitterClient(): TwitterApi {
if (!twitterClient) {
const bearerToken = process.env.TWITTER_BEARER_TOKEN;
if (!bearerToken) {
throw new Error(
"TWITTER_BEARER_TOKEN environment variable is required"
);
}
twitterClient = new TwitterApi(bearerToken);
}
return twitterClient;
}
export async function searchTweets(
query: string,
count: number = 10,
nextToken?: string
): Promise<{ posts: Post[]; nextToken?: string }> {
const client = getTwitterClient();
// Add filter:safe for NSFW filtering as per PRD
const safeQuery = `${query} filter:safe -is:retweet`;
const result = await client.v2.search(safeQuery, {
max_results: Math.min(count, 100),
"tweet.fields": [
"created_at",
"public_metrics",
"author_id",
"attachments",
],
expansions: ["author_id", "attachments.media_keys"],
"media.fields": ["url", "preview_image_url", "type"],
"user.fields": ["name", "username"],
...(nextToken ? { next_token: nextToken } : {}),
});
const users = new Map<string, { name: string; username: string }>();
if (result.includes?.users) {
for (const user of result.includes.users) {
users.set(user.id, { name: user.name, username: user.username });
}
}
const mediaMap = new Map<string, string>();
if (result.includes?.media) {
for (const media of result.includes.media) {
const url = media.url || media.preview_image_url;
if (url) {
mediaMap.set(media.media_key, url);
}
}
}
const posts: Post[] = [];
if (result.data?.data) {
for (const tweet of result.data.data) {
const author = users.get(tweet.author_id || "") || {
name: "Unknown",
username: "unknown",
};
const metrics = tweet.public_metrics || {
like_count: 0,
retweet_count: 0,
reply_count: 0,
};
const mediaKeys = tweet.attachments?.media_keys || [];
const mediaUrls: string[] = [];
for (const key of mediaKeys) {
const url = mediaMap.get(key);
if (url) {
mediaUrls.push(url);
}
}
posts.push({
id: tweet.id,
text: tweet.text,
author: author.name,
author_handle: author.username,
url: `https://twitter.com/${author.username}/status/${tweet.id}`,
created_at: tweet.created_at || new Date().toISOString(),
likes: metrics.like_count || 0,
retweets: metrics.retweet_count || 0,
replies: metrics.reply_count || 0,
has_media: mediaUrls.length > 0,
...(mediaUrls.length > 0 ? { media_urls: mediaUrls } : {}),
});
}
}
return {
posts,
nextToken: result.data?.meta?.next_token,
};
}