From 3dc79689eba7479116b81cc3abbe011930f15bd2 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Wed, 19 Mar 2025 23:22:52 +0800 Subject: [PATCH 1/2] Add tweet listing and improve error handling --- packages/twitter/src/twitter-client.ts | 194 ++++++++++++++++++++----- packages/twitter/src/types.ts | 8 + packages/twitter/src/utils.ts | 33 ++--- 3 files changed, 180 insertions(+), 55 deletions(-) diff --git a/packages/twitter/src/twitter-client.ts b/packages/twitter/src/twitter-client.ts index 719fffe..4f33deb 100644 --- a/packages/twitter/src/twitter-client.ts +++ b/packages/twitter/src/twitter-client.ts @@ -3,7 +3,7 @@ import pThrottle from 'p-throttle' import { z } from 'zod' import type * as types from './types' -import { handleKnownTwitterErrors } from './utils' +import { handleTwitterError } from './utils' /** * This file contains rate-limited wrappers around all of the core Twitter API @@ -14,7 +14,7 @@ import { handleKnownTwitterErrors } from './utils' * denominator OR vary based on the twitter developer plan you're using. We * chose to go with the latter. * - * @see https://developer.twitter.com/en/docs/twitter-api/rate-limits + * @see https://docs.x.com/x-api/fundamentals/rate-limits */ type TwitterApiMethod = @@ -23,6 +23,8 @@ type TwitterApiMethod = | 'findTweetById' | 'findTweetsById' | 'searchRecentTweets' + | 'listTweetsLikedByUserId' + | 'listTweetsByUserId' | 'findUserById' | 'findUserByUsername' @@ -50,6 +52,8 @@ const twitterApiRateLimitsByPlan: Record< findTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, findTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, searchRecentTweets: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + listTweetsLikedByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + listTweetsByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, findUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, findUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS } }, @@ -60,9 +64,9 @@ const twitterApiRateLimitsByPlan: Record< createTweet: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, // https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-mentions - // TODO: undocumented - // 180 per 15m per user - // 450 per 15m per app + // TODO: undocumented? https://docs.x.com/x-api/fundamentals/rate-limits + // 10 per 15m per user + // 10 per 15m per app usersIdMentions: { limit: 180, interval: FIFTEEN_MINUTES_MS }, // 15 per 15m per user @@ -74,6 +78,16 @@ const twitterApiRateLimitsByPlan: Record< // 60 per 15m per app searchRecentTweets: { limit: 60, interval: FIFTEEN_MINUTES_MS }, + // 5 per 15min per user + // 5 per 15min per app + listTweetsLikedByUserId: { limit: 5, interval: FIFTEEN_MINUTES_MS }, + + // 5 per 15min per user + // 10 per 15min per app + listTweetsByUserId: { limit: 5, interval: FIFTEEN_MINUTES_MS }, + + // 100 per 24h per user + // 500 per 24h per app findUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, findUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS } }, @@ -83,21 +97,30 @@ const twitterApiRateLimitsByPlan: Record< // 10k per 24h per app createTweet: { limit: 100, interval: FIFTEEN_MINUTES_MS }, - // 180 per 15m per user + // 300 per 15m per user // 450 per 15m per app - usersIdMentions: { limit: 180, interval: FIFTEEN_MINUTES_MS }, + usersIdMentions: { limit: 300, interval: FIFTEEN_MINUTES_MS }, - // TODO: why would the per-user rate-limit be less than the per-app one?! + // TODO: why would the per-user rate-limit be more than the per-app one?! // 900 per 15m per user // 450 per 15m per app findTweetById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, findTweetsById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, - // TODO: why would the per-user rate-limit be less than the per-app one?! - // 456 per 15m per user - // 300 per 15m per app + // 300 per 15m per user + // 450 per 15m per app searchRecentTweets: { limit: 300, interval: FIFTEEN_MINUTES_MS }, + // 75 per 15min per user + // 75 per 15min per app + listTweetsLikedByUserId: { limit: 75, interval: FIFTEEN_MINUTES_MS }, + + // 900 per 15min per user + // 1500 per 15min per app + listTweetsByUserId: { limit: 900, interval: FIFTEEN_MINUTES_MS }, + + // 900 per 15m per user + // 300 per 15m per app findUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS }, findUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS } }, @@ -107,10 +130,12 @@ const twitterApiRateLimitsByPlan: Record< // completely customizable, but it's still useful to define rate limits // for robustness. These values just 10x those of the pro plan. createTweet: { limit: 1000, interval: FIFTEEN_MINUTES_MS }, - usersIdMentions: { limit: 1800, interval: FIFTEEN_MINUTES_MS }, + usersIdMentions: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, findTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, findTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, searchRecentTweets: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, + listTweetsLikedByUserId: { limit: 750, interval: FIFTEEN_MINUTES_MS }, + listTweetsByUserId: { limit: 9000, interval: FIFTEEN_MINUTES_MS }, findUserById: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, findUserByUsername: { limit: 3000, interval: FIFTEEN_MINUTES_MS } } @@ -150,6 +175,12 @@ export class TwitterClient extends AIFunctionsProvider { const searchRecentTweetsThrottle = pThrottle( twitterApiRateLimits.searchRecentTweets ) + const listTweetsLikedByUserIdThrottle = pThrottle( + twitterApiRateLimits.listTweetsLikedByUserId + ) + const listTweetsByUserIdThrottle = pThrottle( + twitterApiRateLimits.listTweetsByUserId + ) const findUserByIdThrottle = pThrottle(twitterApiRateLimits.findUserById) const findUserByUsernameThrottle = pThrottle( twitterApiRateLimits.findUserByUsername @@ -163,6 +194,12 @@ export class TwitterClient extends AIFunctionsProvider { this._searchRecentTweets = searchRecentTweetsThrottle( searchRecentTweetsImpl(this.client) ) + this._listTweetsLikedByUserId = listTweetsLikedByUserIdThrottle( + listTweetsLikedByUserIdImpl(this.client) + ) + this._listTweetsByUserId = listTweetsByUserIdThrottle( + listTweetsByUserIdImpl(this.client) + ) this._findUserById = findUserByIdThrottle(findUserByIdImpl(this.client)) this._findUserByUsername = findUserByUsernameThrottle( findUserByUsernameImpl(this.client) @@ -173,6 +210,10 @@ export class TwitterClient extends AIFunctionsProvider { protected _findTweetById: ReturnType protected _findTweetsById: ReturnType protected _searchRecentTweets: ReturnType + protected _listTweetsLikedByUserId: ReturnType< + typeof listTweetsLikedByUserIdImpl + > + protected _listTweetsByUserId: ReturnType protected _findUserById: ReturnType protected _findUserByUsername: ReturnType @@ -183,7 +224,7 @@ export class TwitterClient extends AIFunctionsProvider { name: 'create_tweet', description: 'Creates a new tweet', inputSchema: z.object({ - text: z.string().min(1) + text: z.string().nonempty() }) }) async createTweet( @@ -199,13 +240,13 @@ export class TwitterClient extends AIFunctionsProvider { name: 'get_tweet_by_id', description: 'Fetch a tweet by its ID', inputSchema: z.object({ - id: z.string().min(1) + id: z.string().nonempty() }) }) async findTweetById(params: { id: string } & types.FindTweetByIdParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findTweetById not supported on free plan' + 'TwitterClient.findTweetById is not supported on free plan' ) return this._findTweetById(params.id, params) @@ -218,13 +259,13 @@ export class TwitterClient extends AIFunctionsProvider { name: 'get_tweets_by_id', description: 'Fetch an array of tweets by their IDs', inputSchema: z.object({ - ids: z.array(z.string().min(1)) + ids: z.array(z.string().nonempty()) }) }) async findTweetsById({ ids, ...params }: types.FindTweetsByIdParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findTweetsById not supported on free plan' + 'TwitterClient.findTweetsById is not supported on free plan' ) return this._findTweetsById(ids, params) @@ -237,22 +278,78 @@ export class TwitterClient extends AIFunctionsProvider { name: 'search_recent_tweets', description: 'Searches for recent tweets', inputSchema: z.object({ - query: z.string().min(1), + query: z.string().nonempty(), sort_order: z .enum(['recency', 'relevancy']) .default('relevancy') - .optional() + .optional(), + max_results: z.number().min(10).max(100).optional(), + pagination_token: z.string().optional() }) }) async searchRecentTweets(params: types.SearchRecentTweetsParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.searchRecentTweets not supported on free plan' + 'TwitterClient.searchRecentTweets is not supported on free plan' ) return this._searchRecentTweets(params) } + /** + * Lists tweets liked by a user. + */ + @aiFunction({ + name: 'list_tweets_liked_by_user_id', + description: 'Lists tweets liked by a user.', + inputSchema: z.object({ + userId: z.string().nonempty(), + max_results: z.number().min(5).max(100).optional(), + pagination_token: z.string().optional() + }) + }) + async listTweetsLikedByUserId({ + userId, + ...params + }: { userId: string } & types.ListTweetsLikedByUserIdParams) { + assert( + this.twitterApiPlan !== 'free', + 'TwitterClient.listTweetsLikedByUserId is not supported on free plan' + ) + + return this._listTweetsLikedByUserId(userId, params) + } + + /** + * Lists tweets authored by a user. + */ + @aiFunction({ + name: 'list_tweets_by_user_id', + description: 'Lists tweets authored by a user.', + inputSchema: z.object({ + userId: z.string().nonempty(), + max_results: z.number().min(5).max(100).optional(), + pagination_token: z.string().optional(), + exclude: z + .array(z.union([z.literal('replies'), z.literal('retweets')])) + .optional() + .describe( + 'By default, replies and retweets are included. Use this parameter if you want to exclude either or both of them.' + ) + }) + }) + async listTweetsByUserId({ + userId, + ...params + }: { userId: string } & types.ListTweetsByUserIdParams) { + assert( + this.twitterApiPlan !== 'free', + 'TwitterClient.listTweetsByUserId is not supported on free plan' + ) + + return this._listTweetsByUserId(userId, params) + } + /** * Fetch a twitter user by ID */ @@ -380,8 +477,7 @@ function createTweetImpl(client: types.TwitterV2Client) { } catch (err: any) { console.error('error creating tweet', JSON.stringify(err, null, 2)) - handleKnownTwitterErrors(err, { label: 'creating tweet' }) - throw err + handleTwitterError(err, { label: 'error creating tweet' }) } } } @@ -394,8 +490,7 @@ function findTweetByIdImpl(client: types.TwitterV2Client) { ...params }) } catch (err: any) { - handleKnownTwitterErrors(err, { label: `fetching tweet ${tweetId}` }) - throw err + handleTwitterError(err, { label: `error fetching tweet ${tweetId}` }) } } } @@ -412,8 +507,7 @@ function findTweetsByIdImpl(client: types.TwitterV2Client) { ids }) } catch (err: any) { - handleKnownTwitterErrors(err, { label: `fetching ${ids.length} tweets` }) - throw err + handleTwitterError(err, { label: `error fetching ${ids.length} tweets` }) } } } @@ -426,10 +520,9 @@ function searchRecentTweetsImpl(client: types.TwitterV2Client) { ...params }) } catch (err: any) { - handleKnownTwitterErrors(err, { - label: `searching tweets query "${params.query}"` + handleTwitterError(err, { + label: `error searching tweets query "${params.query}"` }) - throw err } } } @@ -442,10 +535,9 @@ function findUserByIdImpl(client: types.TwitterV2Client) { ...params }) } catch (err: any) { - handleKnownTwitterErrors(err, { - label: `fetching user with id ${userId}` + handleTwitterError(err, { + label: `error fetching user ${userId}` }) - throw err } } } @@ -458,10 +550,42 @@ function findUserByUsernameImpl(client: types.TwitterV2Client) { ...params }) } catch (err: any) { - handleKnownTwitterErrors(err, { - label: `fetching user with username ${username}` + handleTwitterError(err, { + label: `error fetching user with username ${username}` + }) + } + } +} + +function listTweetsLikedByUserIdImpl(client: types.TwitterV2Client) { + return async ( + userId: string, + params?: types.ListTweetsLikedByUserIdParams + ) => { + try { + return await client.tweets.usersIdLikedTweets(userId, { + ...defaultTweetQueryParams, + ...params + }) + } catch (err: any) { + handleTwitterError(err, { + label: `error fetching tweets liked by user ${userId}` + }) + } + } +} + +function listTweetsByUserIdImpl(client: types.TwitterV2Client) { + return async (userId: string, params?: types.ListTweetsByUserIdParams) => { + try { + return await client.tweets.usersIdTweets(userId, { + ...defaultTweetQueryParams, + ...params + }) + } catch (err: any) { + handleTwitterError(err, { + label: `error fetching tweets by user ${userId}` }) - throw err } } } diff --git a/packages/twitter/src/types.ts b/packages/twitter/src/types.ts index 18ebbad..61bced2 100644 --- a/packages/twitter/src/types.ts +++ b/packages/twitter/src/types.ts @@ -54,6 +54,14 @@ export type FindUserByUsernameParams = Simplify< Parameters[1] > +export type ListTweetsLikedByUserIdParams = Simplify< + Parameters[1] +> + +export type ListTweetsByUserIdParams = Simplify< + Parameters[1] +> + type Unpacked = T extends (infer U)[] ? U : T export type Tweet = Simplify< diff --git a/packages/twitter/src/utils.ts b/packages/twitter/src/utils.ts index 214ca9b..f4bdaec 100644 --- a/packages/twitter/src/utils.ts +++ b/packages/twitter/src/utils.ts @@ -7,24 +7,21 @@ import { TwitterError } from './error' * Error handler which takes in an unknown Error object and converts it to a * structured TwitterError object for a set of common Twitter API errors. * - * Re-throws the error and will never return. + * Re-throws the error if not recognized and will never return. */ -export function handleKnownTwitterErrors( +export function handleTwitterError( err: any, { label = '' }: { label?: string } = {} -) { +): never { if (err.status === 403) { // user may have deleted the tweet we're trying to respond to - throw new TwitterError( - err.error?.detail || `error ${label}: 403 forbidden`, - { - type: 'twitter:forbidden', - isFinal: true, - cause: err - } - ) + throw new TwitterError(err.error?.detail || `${label}: 403 forbidden`, { + type: 'twitter:forbidden', + isFinal: true, + cause: err + }) } else if (err.status === 401) { - throw new TwitterError(`error ${label}: unauthorized`, { + throw new TwitterError(`${label}: unauthorized`, { type: 'twitter:auth', cause: err }) @@ -34,13 +31,13 @@ export function handleKnownTwitterErrors( err.error?.error_description ) ) { - throw new TwitterError(`error ${label}: invalid auth token`, { + throw new TwitterError(`${label}: invalid auth token`, { type: 'twitter:auth', cause: err }) } } else if (err.status === 429) { - throw new TwitterError(`error ${label}: too many requests`, { + throw new TwitterError(`${label}: too many requests`, { type: 'twitter:rate-limit', cause: err }) @@ -54,9 +51,7 @@ export function handleKnownTwitterErrors( if (err.status >= 400 && err.status < 500) { throw new TwitterError( - `error ${label}: ${err.status} ${ - err.error?.description || err.toString() - }`, + `${label}: ${err.status} ${err.error?.description || err.toString()}`, { type: 'twitter:unknown', isFinal: true, @@ -65,9 +60,7 @@ export function handleKnownTwitterErrors( ) } else if (err.status >= 500) { throw new TwitterError( - `error ${label}: ${err.status} ${ - err.error?.description || err.toString() - }`, + `${label}: ${err.status} ${err.error?.description || err.toString()}`, { type: 'twitter:unknown', isFinal: false, From c6e374a932ff027b625fd31fa22a04bad105380c Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Wed, 19 Mar 2025 23:37:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/twitter/src/twitter-client.ts | 209 ++++++++++++++++--------- packages/twitter/src/types.ts | 10 +- 2 files changed, 139 insertions(+), 80 deletions(-) diff --git a/packages/twitter/src/twitter-client.ts b/packages/twitter/src/twitter-client.ts index 4f33deb..41acf4e 100644 --- a/packages/twitter/src/twitter-client.ts +++ b/packages/twitter/src/twitter-client.ts @@ -19,14 +19,14 @@ import { handleTwitterError } from './utils' type TwitterApiMethod = | 'createTweet' - | 'usersIdMentions' - | 'findTweetById' - | 'findTweetsById' + | 'getTweetById' + | 'getTweetsById' | 'searchRecentTweets' + | 'listTweetMentionsByUserId' | 'listTweetsLikedByUserId' | 'listTweetsByUserId' - | 'findUserById' - | 'findUserByUsername' + | 'getUserById' + | 'getUserByUsername' const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000 const FIFTEEN_MINUTES_MS = 15 * 60 * 1000 @@ -46,16 +46,14 @@ const twitterApiRateLimitsByPlan: Record< // 50 per 24h per app createTweet: { limit: 50, interval: TWENTY_FOUR_HOURS_MS }, - // TODO: according to the twitter docs, this shouldn't be allowed on the - // free plan, but it seems to work... - usersIdMentions: { limit: 1, interval: FIFTEEN_MINUTES_MS }, - findTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, - findTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + getTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + getTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, searchRecentTweets: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + listTweetMentionsByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, listTweetsLikedByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, listTweetsByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, - findUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, - findUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS } + getUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, + getUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS } }, basic: { @@ -63,21 +61,19 @@ const twitterApiRateLimitsByPlan: Record< // 1667 per 24h per app createTweet: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, - // https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-mentions - // TODO: undocumented? https://docs.x.com/x-api/fundamentals/rate-limits - // 10 per 15m per user - // 10 per 15m per app - usersIdMentions: { limit: 180, interval: FIFTEEN_MINUTES_MS }, - // 15 per 15m per user // 15 per 15m per app - findTweetById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, - findTweetsById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, + getTweetById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, + getTweetsById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, // 60 per 15m per user // 60 per 15m per app searchRecentTweets: { limit: 60, interval: FIFTEEN_MINUTES_MS }, + // 10 per 15m per user + // 10 per 15m per app + listTweetMentionsByUserId: { limit: 180, interval: FIFTEEN_MINUTES_MS }, + // 5 per 15min per user // 5 per 15min per app listTweetsLikedByUserId: { limit: 5, interval: FIFTEEN_MINUTES_MS }, @@ -88,8 +84,8 @@ const twitterApiRateLimitsByPlan: Record< // 100 per 24h per user // 500 per 24h per app - findUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, - findUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS } + getUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, + getUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS } }, pro: { @@ -97,20 +93,20 @@ const twitterApiRateLimitsByPlan: Record< // 10k per 24h per app createTweet: { limit: 100, interval: FIFTEEN_MINUTES_MS }, - // 300 per 15m per user - // 450 per 15m per app - usersIdMentions: { limit: 300, interval: FIFTEEN_MINUTES_MS }, - // TODO: why would the per-user rate-limit be more than the per-app one?! // 900 per 15m per user // 450 per 15m per app - findTweetById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, - findTweetsById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, + getTweetById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, + getTweetsById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, // 300 per 15m per user // 450 per 15m per app searchRecentTweets: { limit: 300, interval: FIFTEEN_MINUTES_MS }, + // 300 per 15m per user + // 450 per 15m per app + listTweetMentionsByUserId: { limit: 300, interval: FIFTEEN_MINUTES_MS }, + // 75 per 15min per user // 75 per 15min per app listTweetsLikedByUserId: { limit: 75, interval: FIFTEEN_MINUTES_MS }, @@ -121,8 +117,8 @@ const twitterApiRateLimitsByPlan: Record< // 900 per 15m per user // 300 per 15m per app - findUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS }, - findUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS } + getUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS }, + getUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS } }, enterprise: { @@ -130,17 +126,33 @@ const twitterApiRateLimitsByPlan: Record< // completely customizable, but it's still useful to define rate limits // for robustness. These values just 10x those of the pro plan. createTweet: { limit: 1000, interval: FIFTEEN_MINUTES_MS }, - usersIdMentions: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, - findTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, - findTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, + + getTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, + getTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, + searchRecentTweets: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, + listTweetMentionsByUserId: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, listTweetsLikedByUserId: { limit: 750, interval: FIFTEEN_MINUTES_MS }, listTweetsByUserId: { limit: 9000, interval: FIFTEEN_MINUTES_MS }, - findUserById: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, - findUserByUsername: { limit: 3000, interval: FIFTEEN_MINUTES_MS } + + getUserById: { limit: 3000, interval: FIFTEEN_MINUTES_MS }, + getUserByUsername: { limit: 3000, interval: FIFTEEN_MINUTES_MS } } } +/** + * Twitter API v2 client wrapper with rate-limited methods and `@aiFunction` + * compatibility. + * + * Rate limits differ by plan, so make sure theh `twitterApiPlan` parameter is + * properly set to maximize your rate-limit usage. + * + * @note This class does not handle distributed rate-limits. It assumes a + * single, local client is accessing the API at a time, which is a better fit + * for serverful environments. + * + * @see https://docs.x.com/x-api/fundamentals/rate-limits + */ export class TwitterClient extends AIFunctionsProvider { readonly client: types.TwitterV2Client readonly twitterApiPlan: types.TwitterApiPlan @@ -168,54 +180,59 @@ export class TwitterClient extends AIFunctionsProvider { assert(twitterApiRateLimits, `Invalid twitter api plan: ${twitterApiPlan}`) const createTweetThrottle = pThrottle(twitterApiRateLimits.createTweet) - const findTweetByIdThrottle = pThrottle(twitterApiRateLimits.findTweetById) - const findTweetsByIdThrottle = pThrottle( - twitterApiRateLimits.findTweetsById - ) + const getTweetByIdThrottle = pThrottle(twitterApiRateLimits.getTweetById) + const getTweetsByIdThrottle = pThrottle(twitterApiRateLimits.getTweetsById) const searchRecentTweetsThrottle = pThrottle( twitterApiRateLimits.searchRecentTweets ) + const listTweetMentionsByUserIdThrottle = pThrottle( + twitterApiRateLimits.listTweetMentionsByUserId + ) const listTweetsLikedByUserIdThrottle = pThrottle( twitterApiRateLimits.listTweetsLikedByUserId ) const listTweetsByUserIdThrottle = pThrottle( twitterApiRateLimits.listTweetsByUserId ) - const findUserByIdThrottle = pThrottle(twitterApiRateLimits.findUserById) - const findUserByUsernameThrottle = pThrottle( - twitterApiRateLimits.findUserByUsername + const getUserByIdThrottle = pThrottle(twitterApiRateLimits.getUserById) + const getUserByUsernameThrottle = pThrottle( + twitterApiRateLimits.getUserByUsername ) this._createTweet = createTweetThrottle(createTweetImpl(this.client)) - this._findTweetById = findTweetByIdThrottle(findTweetByIdImpl(this.client)) - this._findTweetsById = findTweetsByIdThrottle( - findTweetsByIdImpl(this.client) - ) + this._getTweetById = getTweetByIdThrottle(getTweetByIdImpl(this.client)) + this._getTweetsById = getTweetsByIdThrottle(getTweetsByIdImpl(this.client)) this._searchRecentTweets = searchRecentTweetsThrottle( searchRecentTweetsImpl(this.client) ) + this._listTweetMentionsByUserId = listTweetMentionsByUserIdThrottle( + listTweetMentionsByUserIdImpl(this.client) + ) this._listTweetsLikedByUserId = listTweetsLikedByUserIdThrottle( listTweetsLikedByUserIdImpl(this.client) ) this._listTweetsByUserId = listTweetsByUserIdThrottle( listTweetsByUserIdImpl(this.client) ) - this._findUserById = findUserByIdThrottle(findUserByIdImpl(this.client)) - this._findUserByUsername = findUserByUsernameThrottle( - findUserByUsernameImpl(this.client) + this._getUserById = getUserByIdThrottle(getUserByIdImpl(this.client)) + this._getUserByUsername = getUserByUsernameThrottle( + getUserByUsernameImpl(this.client) ) } protected _createTweet: ReturnType - protected _findTweetById: ReturnType - protected _findTweetsById: ReturnType + protected _getTweetById: ReturnType + protected _getTweetsById: ReturnType protected _searchRecentTweets: ReturnType + protected _listTweetMentionsByUserId: ReturnType< + typeof listTweetMentionsByUserIdImpl + > protected _listTweetsLikedByUserId: ReturnType< typeof listTweetsLikedByUserIdImpl > protected _listTweetsByUserId: ReturnType - protected _findUserById: ReturnType - protected _findUserByUsername: ReturnType + protected _getUserById: ReturnType + protected _getUserByUsername: ReturnType /** * Creates a new tweet @@ -243,13 +260,13 @@ export class TwitterClient extends AIFunctionsProvider { id: z.string().nonempty() }) }) - async findTweetById(params: { id: string } & types.FindTweetByIdParams) { + async getTweetById(params: { id: string } & types.GetTweetByIdParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findTweetById is not supported on free plan' + 'TwitterClient.getTweetById is not supported on free plan' ) - return this._findTweetById(params.id, params) + return this._getTweetById(params.id, params) } /** @@ -262,13 +279,13 @@ export class TwitterClient extends AIFunctionsProvider { ids: z.array(z.string().nonempty()) }) }) - async findTweetsById({ ids, ...params }: types.FindTweetsByIdParams) { + async getTweetsById({ ids, ...params }: types.GetTweetsByIdParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findTweetsById is not supported on free plan' + 'TwitterClient.getTweetsById is not supported on free plan' ) - return this._findTweetsById(ids, params) + return this._getTweetsById(ids, params) } /** @@ -296,6 +313,30 @@ export class TwitterClient extends AIFunctionsProvider { return this._searchRecentTweets(params) } + /** + * Lists tweets which mention the given user. + */ + @aiFunction({ + name: 'list_tweet_mentions_by_user_id', + description: 'Lists tweets which mention the given user.', + inputSchema: z.object({ + userId: z.string().nonempty(), + max_results: z.number().min(5).max(100).optional(), + pagination_token: z.string().optional() + }) + }) + async listTweetMentionsByUserId({ + userId, + ...params + }: { userId: string } & types.ListTweetMentionsByUserIdParams) { + assert( + this.twitterApiPlan !== 'free', + 'TwitterClient.listTweetMentionsByUserId is not supported on free plan' + ) + + return this._listTweetMentionsByUserId(userId, params) + } + /** * Lists tweets liked by a user. */ @@ -360,16 +401,16 @@ export class TwitterClient extends AIFunctionsProvider { id: z.string().min(1) }) }) - async findUserById({ + async getUserById({ id, ...params - }: { id: string } & types.FindUserByIdParams) { + }: { id: string } & types.GetUserByIdParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findUserById not supported on free plan' + 'TwitterClient.getUserById not supported on free plan' ) - return this._findUserById(id, params) + return this._getUserById(id, params) } /** @@ -382,16 +423,16 @@ export class TwitterClient extends AIFunctionsProvider { username: z.string().min(1) }) }) - async findUserByUsername({ + async getUserByUsername({ username, ...params - }: { username: string } & types.FindUserByUsernameParams) { + }: { username: string } & types.GetUserByUsernameParams) { assert( this.twitterApiPlan !== 'free', - 'TwitterClient.findUserByUsername not supported on free plan' + 'TwitterClient.getUserByUsername not supported on free plan' ) - return this._findUserByUsername(username, params) + return this._getUserByUsername(username, params) } } @@ -482,8 +523,8 @@ function createTweetImpl(client: types.TwitterV2Client) { } } -function findTweetByIdImpl(client: types.TwitterV2Client) { - return async (tweetId: string, params?: types.FindTweetByIdParams) => { +function getTweetByIdImpl(client: types.TwitterV2Client) { + return async (tweetId: string, params?: types.GetTweetByIdParams) => { try { return await client.tweets.findTweetById(tweetId, { ...defaultTweetQueryParams, @@ -495,10 +536,10 @@ function findTweetByIdImpl(client: types.TwitterV2Client) { } } -function findTweetsByIdImpl(client: types.TwitterV2Client) { +function getTweetsByIdImpl(client: types.TwitterV2Client) { return async ( ids: string[], - params?: Omit + params?: Omit ) => { try { return await client.tweets.findTweetsById({ @@ -527,8 +568,8 @@ function searchRecentTweetsImpl(client: types.TwitterV2Client) { } } -function findUserByIdImpl(client: types.TwitterV2Client) { - return async (userId: string, params?: types.FindUserByIdParams) => { +function getUserByIdImpl(client: types.TwitterV2Client) { + return async (userId: string, params?: types.GetUserByIdParams) => { try { return await client.users.findUserById(userId, { ...defaultUserQueryParams, @@ -542,8 +583,8 @@ function findUserByIdImpl(client: types.TwitterV2Client) { } } -function findUserByUsernameImpl(client: types.TwitterV2Client) { - return async (username: string, params?: types.FindUserByUsernameParams) => { +function getUserByUsernameImpl(client: types.TwitterV2Client) { + return async (username: string, params?: types.GetUserByUsernameParams) => { try { return await client.users.findUserByUsername(username, { ...defaultUserQueryParams, @@ -557,6 +598,24 @@ function findUserByUsernameImpl(client: types.TwitterV2Client) { } } +function listTweetMentionsByUserIdImpl(client: types.TwitterV2Client) { + return async ( + userId: string, + params?: types.ListTweetMentionsByUserIdParams + ) => { + try { + return await client.tweets.usersIdMentions(userId, { + ...defaultTweetQueryParams, + ...params + }) + } catch (err: any) { + handleTwitterError(err, { + label: `error fetching tweets mentions for user ${userId}` + }) + } + } +} + function listTweetsLikedByUserIdImpl(client: types.TwitterV2Client) { return async ( userId: string, diff --git a/packages/twitter/src/types.ts b/packages/twitter/src/types.ts index 61bced2..11b50b9 100644 --- a/packages/twitter/src/types.ts +++ b/packages/twitter/src/types.ts @@ -30,15 +30,15 @@ export type CreateTweetParams = Simplify< Parameters[0] > -export type UsersIdMentionsParams = Simplify< +export type ListTweetMentionsByUserIdParams = Simplify< Parameters[1] > -export type FindTweetByIdParams = Simplify< +export type GetTweetByIdParams = Simplify< Parameters[1] > -export type FindTweetsByIdParams = Simplify< +export type GetTweetsByIdParams = Simplify< Parameters[0] > @@ -46,11 +46,11 @@ export type SearchRecentTweetsParams = Simplify< Parameters[0] > -export type FindUserByIdParams = Simplify< +export type GetUserByIdParams = Simplify< Parameters[1] > -export type FindUserByUsernameParams = Simplify< +export type GetUserByUsernameParams = Simplify< Parameters[1] >