remotes/origin/feature/update-twitter-nango
Travis Fischer 2025-03-19 23:37:00 +08:00
rodzic 3dc79689eb
commit c6e374a932
2 zmienionych plików z 139 dodań i 80 usunięć

Wyświetl plik

@ -19,14 +19,14 @@ import { handleTwitterError } from './utils'
type TwitterApiMethod = type TwitterApiMethod =
| 'createTweet' | 'createTweet'
| 'usersIdMentions' | 'getTweetById'
| 'findTweetById' | 'getTweetsById'
| 'findTweetsById'
| 'searchRecentTweets' | 'searchRecentTweets'
| 'listTweetMentionsByUserId'
| 'listTweetsLikedByUserId' | 'listTweetsLikedByUserId'
| 'listTweetsByUserId' | 'listTweetsByUserId'
| 'findUserById' | 'getUserById'
| 'findUserByUsername' | 'getUserByUsername'
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000 const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000
const FIFTEEN_MINUTES_MS = 15 * 60 * 1000 const FIFTEEN_MINUTES_MS = 15 * 60 * 1000
@ -46,16 +46,14 @@ const twitterApiRateLimitsByPlan: Record<
// 50 per 24h per app // 50 per 24h per app
createTweet: { limit: 50, interval: TWENTY_FOUR_HOURS_MS }, createTweet: { limit: 50, interval: TWENTY_FOUR_HOURS_MS },
// TODO: according to the twitter docs, this shouldn't be allowed on the getTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
// free plan, but it seems to work... getTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
usersIdMentions: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
searchRecentTweets: { 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 }, listTweetsLikedByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS },
listTweetsByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS }, listTweetsByUserId: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS }, getUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS } getUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS }
}, },
basic: { basic: {
@ -63,21 +61,19 @@ const twitterApiRateLimitsByPlan: Record<
// 1667 per 24h per app // 1667 per 24h per app
createTweet: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, 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 user
// 15 per 15m per app // 15 per 15m per app
findTweetById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, getTweetById: { limit: 15, interval: FIFTEEN_MINUTES_MS },
findTweetsById: { limit: 15, interval: FIFTEEN_MINUTES_MS }, getTweetsById: { limit: 15, interval: FIFTEEN_MINUTES_MS },
// 60 per 15m per user // 60 per 15m per user
// 60 per 15m per app // 60 per 15m per app
searchRecentTweets: { limit: 60, interval: FIFTEEN_MINUTES_MS }, 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 user
// 5 per 15min per app // 5 per 15min per app
listTweetsLikedByUserId: { limit: 5, interval: FIFTEEN_MINUTES_MS }, listTweetsLikedByUserId: { limit: 5, interval: FIFTEEN_MINUTES_MS },
@ -88,8 +84,8 @@ const twitterApiRateLimitsByPlan: Record<
// 100 per 24h per user // 100 per 24h per user
// 500 per 24h per app // 500 per 24h per app
findUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }, getUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS },
findUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS } getUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }
}, },
pro: { pro: {
@ -97,20 +93,20 @@ const twitterApiRateLimitsByPlan: Record<
// 10k per 24h per app // 10k per 24h per app
createTweet: { limit: 100, interval: FIFTEEN_MINUTES_MS }, 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?! // TODO: why would the per-user rate-limit be more than the per-app one?!
// 900 per 15m per user // 900 per 15m per user
// 450 per 15m per app // 450 per 15m per app
findTweetById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, getTweetById: { limit: 450, interval: FIFTEEN_MINUTES_MS },
findTweetsById: { limit: 450, interval: FIFTEEN_MINUTES_MS }, getTweetsById: { limit: 450, interval: FIFTEEN_MINUTES_MS },
// 300 per 15m per user // 300 per 15m per user
// 450 per 15m per app // 450 per 15m per app
searchRecentTweets: { limit: 300, interval: FIFTEEN_MINUTES_MS }, 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 user
// 75 per 15min per app // 75 per 15min per app
listTweetsLikedByUserId: { limit: 75, interval: FIFTEEN_MINUTES_MS }, listTweetsLikedByUserId: { limit: 75, interval: FIFTEEN_MINUTES_MS },
@ -121,8 +117,8 @@ const twitterApiRateLimitsByPlan: Record<
// 900 per 15m per user // 900 per 15m per user
// 300 per 15m per app // 300 per 15m per app
findUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS }, getUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS },
findUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS } getUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS }
}, },
enterprise: { enterprise: {
@ -130,17 +126,33 @@ const twitterApiRateLimitsByPlan: Record<
// completely customizable, but it's still useful to define rate limits // completely customizable, but it's still useful to define rate limits
// for robustness. These values just 10x those of the pro plan. // for robustness. These values just 10x those of the pro plan.
createTweet: { limit: 1000, interval: FIFTEEN_MINUTES_MS }, createTweet: { limit: 1000, interval: FIFTEEN_MINUTES_MS },
usersIdMentions: { limit: 3000, interval: FIFTEEN_MINUTES_MS },
findTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, getTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS },
findTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS }, getTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS },
searchRecentTweets: { limit: 3000, 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 }, listTweetsLikedByUserId: { limit: 750, interval: FIFTEEN_MINUTES_MS },
listTweetsByUserId: { limit: 9000, 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 { export class TwitterClient extends AIFunctionsProvider {
readonly client: types.TwitterV2Client readonly client: types.TwitterV2Client
readonly twitterApiPlan: types.TwitterApiPlan readonly twitterApiPlan: types.TwitterApiPlan
@ -168,54 +180,59 @@ export class TwitterClient extends AIFunctionsProvider {
assert(twitterApiRateLimits, `Invalid twitter api plan: ${twitterApiPlan}`) assert(twitterApiRateLimits, `Invalid twitter api plan: ${twitterApiPlan}`)
const createTweetThrottle = pThrottle(twitterApiRateLimits.createTweet) const createTweetThrottle = pThrottle(twitterApiRateLimits.createTweet)
const findTweetByIdThrottle = pThrottle(twitterApiRateLimits.findTweetById) const getTweetByIdThrottle = pThrottle(twitterApiRateLimits.getTweetById)
const findTweetsByIdThrottle = pThrottle( const getTweetsByIdThrottle = pThrottle(twitterApiRateLimits.getTweetsById)
twitterApiRateLimits.findTweetsById
)
const searchRecentTweetsThrottle = pThrottle( const searchRecentTweetsThrottle = pThrottle(
twitterApiRateLimits.searchRecentTweets twitterApiRateLimits.searchRecentTweets
) )
const listTweetMentionsByUserIdThrottle = pThrottle(
twitterApiRateLimits.listTweetMentionsByUserId
)
const listTweetsLikedByUserIdThrottle = pThrottle( const listTweetsLikedByUserIdThrottle = pThrottle(
twitterApiRateLimits.listTweetsLikedByUserId twitterApiRateLimits.listTweetsLikedByUserId
) )
const listTweetsByUserIdThrottle = pThrottle( const listTweetsByUserIdThrottle = pThrottle(
twitterApiRateLimits.listTweetsByUserId twitterApiRateLimits.listTweetsByUserId
) )
const findUserByIdThrottle = pThrottle(twitterApiRateLimits.findUserById) const getUserByIdThrottle = pThrottle(twitterApiRateLimits.getUserById)
const findUserByUsernameThrottle = pThrottle( const getUserByUsernameThrottle = pThrottle(
twitterApiRateLimits.findUserByUsername twitterApiRateLimits.getUserByUsername
) )
this._createTweet = createTweetThrottle(createTweetImpl(this.client)) this._createTweet = createTweetThrottle(createTweetImpl(this.client))
this._findTweetById = findTweetByIdThrottle(findTweetByIdImpl(this.client)) this._getTweetById = getTweetByIdThrottle(getTweetByIdImpl(this.client))
this._findTweetsById = findTweetsByIdThrottle( this._getTweetsById = getTweetsByIdThrottle(getTweetsByIdImpl(this.client))
findTweetsByIdImpl(this.client)
)
this._searchRecentTweets = searchRecentTweetsThrottle( this._searchRecentTweets = searchRecentTweetsThrottle(
searchRecentTweetsImpl(this.client) searchRecentTweetsImpl(this.client)
) )
this._listTweetMentionsByUserId = listTweetMentionsByUserIdThrottle(
listTweetMentionsByUserIdImpl(this.client)
)
this._listTweetsLikedByUserId = listTweetsLikedByUserIdThrottle( this._listTweetsLikedByUserId = listTweetsLikedByUserIdThrottle(
listTweetsLikedByUserIdImpl(this.client) listTweetsLikedByUserIdImpl(this.client)
) )
this._listTweetsByUserId = listTweetsByUserIdThrottle( this._listTweetsByUserId = listTweetsByUserIdThrottle(
listTweetsByUserIdImpl(this.client) listTweetsByUserIdImpl(this.client)
) )
this._findUserById = findUserByIdThrottle(findUserByIdImpl(this.client)) this._getUserById = getUserByIdThrottle(getUserByIdImpl(this.client))
this._findUserByUsername = findUserByUsernameThrottle( this._getUserByUsername = getUserByUsernameThrottle(
findUserByUsernameImpl(this.client) getUserByUsernameImpl(this.client)
) )
} }
protected _createTweet: ReturnType<typeof createTweetImpl> protected _createTweet: ReturnType<typeof createTweetImpl>
protected _findTweetById: ReturnType<typeof findTweetByIdImpl> protected _getTweetById: ReturnType<typeof getTweetByIdImpl>
protected _findTweetsById: ReturnType<typeof findTweetsByIdImpl> protected _getTweetsById: ReturnType<typeof getTweetsByIdImpl>
protected _searchRecentTweets: ReturnType<typeof searchRecentTweetsImpl> protected _searchRecentTweets: ReturnType<typeof searchRecentTweetsImpl>
protected _listTweetMentionsByUserId: ReturnType<
typeof listTweetMentionsByUserIdImpl
>
protected _listTweetsLikedByUserId: ReturnType< protected _listTweetsLikedByUserId: ReturnType<
typeof listTweetsLikedByUserIdImpl typeof listTweetsLikedByUserIdImpl
> >
protected _listTweetsByUserId: ReturnType<typeof listTweetsByUserIdImpl> protected _listTweetsByUserId: ReturnType<typeof listTweetsByUserIdImpl>
protected _findUserById: ReturnType<typeof findUserByIdImpl> protected _getUserById: ReturnType<typeof getUserByIdImpl>
protected _findUserByUsername: ReturnType<typeof findUserByUsernameImpl> protected _getUserByUsername: ReturnType<typeof getUserByUsernameImpl>
/** /**
* Creates a new tweet * Creates a new tweet
@ -243,13 +260,13 @@ export class TwitterClient extends AIFunctionsProvider {
id: z.string().nonempty() id: z.string().nonempty()
}) })
}) })
async findTweetById(params: { id: string } & types.FindTweetByIdParams) { async getTweetById(params: { id: string } & types.GetTweetByIdParams) {
assert( assert(
this.twitterApiPlan !== 'free', 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()) ids: z.array(z.string().nonempty())
}) })
}) })
async findTweetsById({ ids, ...params }: types.FindTweetsByIdParams) { async getTweetsById({ ids, ...params }: types.GetTweetsByIdParams) {
assert( assert(
this.twitterApiPlan !== 'free', 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) 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. * Lists tweets liked by a user.
*/ */
@ -360,16 +401,16 @@ export class TwitterClient extends AIFunctionsProvider {
id: z.string().min(1) id: z.string().min(1)
}) })
}) })
async findUserById({ async getUserById({
id, id,
...params ...params
}: { id: string } & types.FindUserByIdParams) { }: { id: string } & types.GetUserByIdParams) {
assert( assert(
this.twitterApiPlan !== 'free', 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) username: z.string().min(1)
}) })
}) })
async findUserByUsername({ async getUserByUsername({
username, username,
...params ...params
}: { username: string } & types.FindUserByUsernameParams) { }: { username: string } & types.GetUserByUsernameParams) {
assert( assert(
this.twitterApiPlan !== 'free', 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) { function getTweetByIdImpl(client: types.TwitterV2Client) {
return async (tweetId: string, params?: types.FindTweetByIdParams) => { return async (tweetId: string, params?: types.GetTweetByIdParams) => {
try { try {
return await client.tweets.findTweetById(tweetId, { return await client.tweets.findTweetById(tweetId, {
...defaultTweetQueryParams, ...defaultTweetQueryParams,
@ -495,10 +536,10 @@ function findTweetByIdImpl(client: types.TwitterV2Client) {
} }
} }
function findTweetsByIdImpl(client: types.TwitterV2Client) { function getTweetsByIdImpl(client: types.TwitterV2Client) {
return async ( return async (
ids: string[], ids: string[],
params?: Omit<types.FindTweetsByIdParams, 'ids'> params?: Omit<types.GetTweetsByIdParams, 'ids'>
) => { ) => {
try { try {
return await client.tweets.findTweetsById({ return await client.tweets.findTweetsById({
@ -527,8 +568,8 @@ function searchRecentTweetsImpl(client: types.TwitterV2Client) {
} }
} }
function findUserByIdImpl(client: types.TwitterV2Client) { function getUserByIdImpl(client: types.TwitterV2Client) {
return async (userId: string, params?: types.FindUserByIdParams) => { return async (userId: string, params?: types.GetUserByIdParams) => {
try { try {
return await client.users.findUserById(userId, { return await client.users.findUserById(userId, {
...defaultUserQueryParams, ...defaultUserQueryParams,
@ -542,8 +583,8 @@ function findUserByIdImpl(client: types.TwitterV2Client) {
} }
} }
function findUserByUsernameImpl(client: types.TwitterV2Client) { function getUserByUsernameImpl(client: types.TwitterV2Client) {
return async (username: string, params?: types.FindUserByUsernameParams) => { return async (username: string, params?: types.GetUserByUsernameParams) => {
try { try {
return await client.users.findUserByUsername(username, { return await client.users.findUserByUsername(username, {
...defaultUserQueryParams, ...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) { function listTweetsLikedByUserIdImpl(client: types.TwitterV2Client) {
return async ( return async (
userId: string, userId: string,

Wyświetl plik

@ -30,15 +30,15 @@ export type CreateTweetParams = Simplify<
Parameters<TwitterV2Client['tweets']['createTweet']>[0] Parameters<TwitterV2Client['tweets']['createTweet']>[0]
> >
export type UsersIdMentionsParams = Simplify< export type ListTweetMentionsByUserIdParams = Simplify<
Parameters<TwitterV2Client['tweets']['usersIdMentions']>[1] Parameters<TwitterV2Client['tweets']['usersIdMentions']>[1]
> >
export type FindTweetByIdParams = Simplify< export type GetTweetByIdParams = Simplify<
Parameters<TwitterV2Client['tweets']['findTweetById']>[1] Parameters<TwitterV2Client['tweets']['findTweetById']>[1]
> >
export type FindTweetsByIdParams = Simplify< export type GetTweetsByIdParams = Simplify<
Parameters<TwitterV2Client['tweets']['findTweetsById']>[0] Parameters<TwitterV2Client['tweets']['findTweetsById']>[0]
> >
@ -46,11 +46,11 @@ export type SearchRecentTweetsParams = Simplify<
Parameters<TwitterV2Client['tweets']['tweetsRecentSearch']>[0] Parameters<TwitterV2Client['tweets']['tweetsRecentSearch']>[0]
> >
export type FindUserByIdParams = Simplify< export type GetUserByIdParams = Simplify<
Parameters<TwitterV2Client['users']['findUserById']>[1] Parameters<TwitterV2Client['users']['findUserById']>[1]
> >
export type FindUserByUsernameParams = Simplify< export type GetUserByUsernameParams = Simplify<
Parameters<TwitterV2Client['users']['findUserByUsername']>[1] Parameters<TwitterV2Client['users']['findUserByUsername']>[1]
> >