import { BasePlatform, type ActorConfig } from './base.js';
import type { UnifiedPost } from '../types.js';
export class TwitterPlatform extends BasePlatform {
readonly config: ActorConfig = {
actorId: 'apidojo/tweet-scraper',
platform: 'twitter',
defaultMaxResults: 50,
};
buildInput(params: Record<string, unknown>): Record<string, unknown> {
const input: Record<string, unknown> = {
maxItems: params.max_results ?? this.config.defaultMaxResults,
};
if (params.query) {
input.searchTerms = [params.query as string];
input.searchMode = 'live';
}
if (params.type === 'user' && params.query) {
input.twitterHandles = [params.query as string];
delete input.searchTerms;
}
if (params.date_from) input.sinceDate = params.date_from;
if (params.date_to) input.untilDate = params.date_to;
return input;
}
normalize(raw: Record<string, unknown>[]): UnifiedPost[] {
return raw.map(item => {
const author = item.author as Record<string, unknown> | undefined;
const text = this.safeString(item.full_text ?? item.text ?? item.tweetText ?? '');
return {
id: this.makeId(this.safeString(item.id ?? item.tweetId ?? item.id_str)),
platform: 'twitter' as const,
author: {
username: this.safeString(author?.userName ?? author?.screen_name ?? item.username ?? ''),
displayName: this.safeString(author?.name ?? author?.displayName ?? ''),
followers: this.safeNumber(author?.followers ?? author?.followers_count),
verified: Boolean(author?.isVerified ?? author?.verified),
},
content: text,
url: this.safeString(item.url ?? item.tweetUrl ?? ''),
timestamp: this.safeDate(item.createdAt ?? item.created_at ?? item.timestamp),
engagement: {
likes: this.safeNumber(item.likeCount ?? item.favorite_count ?? item.likes),
comments: this.safeNumber(item.replyCount ?? item.reply_count ?? item.replies),
shares: this.safeNumber(item.retweetCount ?? item.retweet_count ?? item.retweets),
views: this.safeNumber(item.viewCount ?? item.views),
},
media: this.extractMedia(item),
hashtags: this.extractHashtags(text),
metadata: {
isRetweet: Boolean(item.isRetweet),
isReply: Boolean(item.isReply),
language: item.lang,
source: item.source,
},
};
});
}
private extractMedia(item: Record<string, unknown>): { type: 'image' | 'video' | 'text'; urls: string[] } | undefined {
const media = item.media as Record<string, unknown>[] | undefined;
if (!media?.length) return undefined;
const urls = media.map(m => this.safeString(m.url ?? m.media_url_https ?? ''));
const hasVideo = media.some(m => m.type === 'video' || m.type === 'animated_gif');
return { type: hasVideo ? 'video' : 'image', urls: urls.filter(Boolean) };
}
}