feat: add twitter client

pull/643/head^2
Travis Fischer 2024-06-04 22:10:19 -05:00
rodzic 24857b1b34
commit 9e84229c16
13 zmienionych plików z 835 dodań i 131 usunięć

Wyświetl plik

@ -11,7 +11,11 @@ import restoreCursor from 'restore-cursor'
// import { FirecrawlClient } from '../src/index.js'
// import { ExaClient } from '../src/index.js'
// import { DiffbotClient } from '../src/index.js'
import { WolframClient } from '../src/index.js'
// import { WolframClient } from '../src/index.js'
import {
createTwitterV2Client,
TwitterClient
} from '../src/services/twitter/index.js'
/**
* Scratch pad for testing.
@ -76,14 +80,19 @@ async function main() {
// })
// console.log(JSON.stringify(res, null, 2))
const wolfram = new WolframClient()
// const res = await diffbot.analyzeUrl({
// url: 'https://www.bbc.com/news/articles/cp4475gwny1o'
// const wolfram = new WolframClient()
// const res = await wolfram.ask({
// input: 'population of new york city'
// })
const res = await wolfram.ask({
input: 'population of new york city'
// console.log(res)
const client = await createTwitterV2Client({
// scopes: ['tweet.read', 'users.read', 'offline.access']
})
console.log(res)
const twitter = new TwitterClient({ client })
const user = await twitter.findUserByUsername({ username: 'transitive_bs' })
console.log(user)
}
try {

Wyświetl plik

@ -38,10 +38,25 @@
"import": "./dist/sdks/genkit.js",
"default": "./dist/sdks/genkit.js"
},
"./langchain": {
"types": "./dist/sdks/langchain.d.ts",
"import": "./dist/sdks/langchain.js",
"default": "./dist/sdks/langchain.js"
},
"./llamaindex": {
"types": "./dist/sdks/llamaindex.d.ts",
"import": "./dist/sdks/llamaindex.js",
"default": "./dist/sdks/llamaindex.js"
},
"./calculator": {
"types": "./dist/tools/calculator.d.ts",
"import": "./dist/tools/calculator.js",
"default": "./dist/tools/calculator.js"
},
"./twitter": {
"types": "./dist/services/twitter/index.d.ts",
"import": "./dist/services/twitter/index.js",
"default": "./dist/services/twitter/index.js"
}
},
"files": [
@ -74,7 +89,6 @@
"p-map": "^7.0.2",
"p-throttle": "^6.1.0",
"quick-lru": "^7.0.0",
"twitter-api-sdk": "^1.2.1",
"type-fest": "^4.18.3",
"zod": "^3.23.3",
"zod-to-json-schema": "^3.23.0"
@ -104,6 +118,7 @@
"ts-node": "^10.9.2",
"tsup": "^8.0.2",
"tsx": "^4.11.0",
"twitter-api-sdk": "^1.2.1",
"typescript": "^5.4.5",
"vitest": "2.0.0-beta.3"
},
@ -113,7 +128,8 @@
"@langchain/core": "^0.2.5",
"ai": "^3.1.22",
"expr-eval": "^2.0.2",
"llamaindex": "^0.3.15"
"llamaindex": "^0.3.15",
"twitter-api-sdk": "^1.2.1"
},
"peerDependenciesMeta": {
"@dexaai/dexter": {
@ -125,13 +141,16 @@
"@langchain/core": {
"optional": true
},
"ai": {
"optional": true
},
"expr-eval": {
"optional": true
},
"llamaindex": {
"optional": true
},
"ai": {
"twitter-api-sdk": {
"optional": true
}
},

Wyświetl plik

@ -116,9 +116,10 @@ The SDK-specific imports are all isolated to keep the main `@agentic/stdlib` as
- SDK adaptors should be as lightweight as possible and be optional peer dependencies of `@agentic/stdlib`
- SDK adatptor entrypoints should all be isolated to their own top-level imports
- `@agentic/stdlib/ai-sdk`
- `@agentic/stdlib/langchain`
- `@agentic/stdlib/llamaindex`
- `@agentic/stdlib/dexter`
- `@agentic/stdlib/genkit`
- `@agentic/stdlib/langchain`
## Services
@ -136,23 +137,28 @@ The SDK-specific imports are all isolated to keep the main `@agentic/stdlib` as
- serpapi
- serper
- twitter (WIP)
- wolfram alpha
- weatherapi
- wikipedia
## AI SDKs
- openai sdk
- vercel ai sdk
- dexa dexter
- firebase genkit
- langchain
- llamaindex
- OpenAI SDK
- no need for an adaptor; use `AIFunctionSet.specs` or `AIFunctionSet.toolSpecs`
- Vercel AI SDK
- `import { createAISDKTools } from '@agentic/stdlib/ai-sdk'`
- LangChain
- `import { createLangChainTools } from '@agentic/stdlib/langchain'`
- LlamaIndex
- `import { createLlamaIndexTools } from '@agentic/stdlib/llamaindex'`
- Firebase Genkit
- `import { createGenkitTools } from '@agentic/stdlib/genkit'`
- Dexa Dexter
- `import { createDexterFunctions } from '@agentic/stdlib/dexter'`
## TODO
- rename this repo to agentic
- sdks
- TODO
- services
- e2b
- search-and-scrape

Wyświetl plik

@ -1,9 +1,9 @@
export * from './ai-function-set.js'
export * from './create-ai-function.js'
export * from './create-ai-function.js'
export * from './errors.js'
export * from './fns.js'
export * from './message.js'
export * from './nango.js'
export * from './parse-structured-output.js'
export * from './services/index.js'
export type * from './types.js'

47
src/nango.ts 100644
Wyświetl plik

@ -0,0 +1,47 @@
import { type Connection, Nango } from '@nangohq/node'
import { getEnv } from './utils.js'
// This is intentionally left as a global singleton to avoid re-creating the
// Nango connection instance on successive calls in serverless environments.
let _nango: Nango | null = null
export function getNango(): Nango {
if (!_nango) {
const secretKey = getEnv('NANGO_SECRET_KEY')?.trim()
if (!secretKey) {
throw new Error(`Missing required "NANGO_SECRET_KEY"`)
}
_nango = new Nango({ secretKey })
}
return _nango
}
export function validateNangoConnectionOAuthScopes({
connection,
scopes
}: {
connection: Connection
scopes: string[]
}) {
const connectionScopes = new Set<string>(
connection.credentials.raw.scope.split(' ')
)
const missingScopes = new Set<string>()
for (const scope of scopes) {
if (!connectionScopes.has(scope)) {
missingScopes.add(scope)
}
}
if (missingScopes.size > 0) {
throw new Error(
`Nango connection ${connection.id} is missing required OAuth scopes: ${[
...missingScopes.values()
].join(', ')}`
)
}
}

Wyświetl plik

@ -1,111 +0,0 @@
import { Nango } from '@nangohq/node'
import { auth, Client as TwitterClient } from 'twitter-api-sdk'
import { assert, getEnv } from '../utils.js'
// The Twitter+Nango client auth connection key
const nangoTwitterProviderConfigKey = 'twitter-v2'
// The Twitter OAuth2User class requires a client id, which we don't have
// since we're using Nango for auth, so instead we just pass a dummy value
// and allow Nango to handle all auth/refresh/access token management.
const twitterClientId = 'xbot'
const defaultRequiredTwitterOAuthScopes = new Set<string>([
'tweet.read',
'users.read',
'offline.access',
'tweet.write'
])
let _nango: Nango | null = null
function getNango(): Nango {
if (!_nango) {
const secretKey = getEnv('NANGO_SECRET_KEY')?.trim()
if (!secretKey) {
throw new Error(`Missing required "NANGO_SECRET_KEY"`)
}
_nango = new Nango({ secretKey })
}
return _nango
}
async function getTwitterAuth({
scopes,
nangoConnectionId,
nangoCallbackUrl
}: {
scopes: Set<string>
nangoConnectionId: string
nangoCallbackUrl: string
}): Promise<auth.OAuth2User> {
const nango = getNango()
const connection = await nango.getConnection(
nangoTwitterProviderConfigKey,
nangoConnectionId
)
// console.debug('nango twitter connection', connection)
// connection.credentials.raw
// {
// token_type: 'bearer',
// expires_in: number,
// access_token: string
// scope: string
// expires_at: string
// }
const connectionScopes = new Set<string>(
connection.credentials.raw.scope.split(' ')
)
const missingScopes = new Set<string>()
for (const scope of scopes) {
if (!connectionScopes.has(scope)) {
missingScopes.add(scope)
}
}
if (missingScopes.size > 0) {
throw new Error(
`Nango connection ${nangoConnectionId} is missing required OAuth scopes: ${[
...missingScopes.values()
].join(', ')}`
)
}
const token = connection.credentials.raw
assert(token)
return new auth.OAuth2User({
client_id: twitterClientId,
callback: nangoCallbackUrl,
scopes: [...scopes.values()] as any,
token
})
}
export async function getTwitterClient({
scopes = defaultRequiredTwitterOAuthScopes,
nangoConnectionId = getEnv('NANGO_CONNECTION_ID'),
nangoCallbackUrl = getEnv('NANGO_CALLBACK_URL')
}: {
scopes?: Set<string>
nangoConnectionId?: string
nangoCallbackUrl?: string
} = {}): Promise<TwitterClient> {
assert(nangoConnectionId, 'twitter client missing nangoConnectionId')
assert(nangoCallbackUrl, 'twitter client missing nangoCallbackUrl')
// NOTE: Nango handles refreshing the oauth access token for us
const twitterAuth = await getTwitterAuth({
scopes,
nangoConnectionId,
nangoCallbackUrl
})
// Twitter API v2 using OAuth 2.0
return new TwitterClient(twitterAuth)
}

Wyświetl plik

@ -0,0 +1,78 @@
import { auth, Client as TwitterV2Client } from 'twitter-api-sdk'
import { getNango, validateNangoConnectionOAuthScopes } from '../../nango.js'
import { assert, getEnv } from '../../utils.js'
// Auth new Nango accounts here: https://app.nango.dev/connections
// The Twitter OAuth2User class requires a client id, which we don't have
// since we're using Nango for auth, so instead we just pass a dummy value
// and allow Nango to handle all auth/refresh/access token management.
const dummyTwitterClientId = 'agentic'
export const defaultTwitterOAuthScopes = [
'tweet.read',
'users.read',
'offline.access',
'tweet.write'
]
async function createTwitterAuth({
scopes,
nangoConnectionId,
nangoCallbackUrl,
nangoProviderConfigKey
}: {
scopes: string[]
nangoConnectionId: string
nangoCallbackUrl: string
nangoProviderConfigKey: string
}): Promise<auth.OAuth2User> {
const nango = getNango()
const connection = await nango.getConnection(
nangoProviderConfigKey,
nangoConnectionId
)
validateNangoConnectionOAuthScopes({
connection,
scopes
})
const token = connection.credentials.raw
assert(token)
return new auth.OAuth2User({
client_id: dummyTwitterClientId,
callback: nangoCallbackUrl,
scopes: scopes as any[],
token
})
}
export async function createTwitterV2Client({
scopes = defaultTwitterOAuthScopes,
nangoConnectionId = getEnv('NANGO_CONNECTION_ID'),
nangoCallbackUrl = getEnv('NANGO_CALLBACK_URL') ??
'https://api.nango.dev/oauth/callback',
nangoProviderConfigKey = 'twitter-v2'
}: {
scopes?: string[]
nangoConnectionId?: string
nangoCallbackUrl?: string
nangoProviderConfigKey?: string
} = {}): Promise<TwitterV2Client> {
assert(nangoConnectionId, 'twitter client missing nangoConnectionId')
assert(nangoCallbackUrl, 'twitter client missing nangoCallbackUrl')
// NOTE: Nango handles refreshing the oauth access token for us
const twitterAuth = await createTwitterAuth({
scopes,
nangoConnectionId,
nangoCallbackUrl,
nangoProviderConfigKey
})
// Twitter API v2 using OAuth 2.0
return new TwitterV2Client(twitterAuth)
}

Wyświetl plik

@ -0,0 +1,32 @@
export type TwitterErrorType =
| 'twitter:forbidden'
| 'twitter:auth'
| 'twitter:rate-limit'
| 'twitter:unknown'
| 'network'
export class TwitterError extends Error {
type: TwitterErrorType
isFinal: boolean
status?: number
constructor(
message: string,
{
type,
isFinal = false,
status,
...opts
}: ErrorOptions & {
type: TwitterErrorType
isFinal?: boolean
status?: number
}
) {
super(message, opts)
this.type = type
this.isFinal = isFinal
this.status = status ?? (opts.cause as any)?.status
}
}

Wyświetl plik

@ -0,0 +1,5 @@
export * from './client.js'
export * from './error.js'
export * from './twitter-client.js'
export type * from './types.js'
export * from './utils.js'

Wyświetl plik

@ -0,0 +1,405 @@
import pThrottle from 'p-throttle'
import { z } from 'zod'
import type * as types from './types.js'
import { aiFunction, AIFunctionsProvider } from '../../fns.js'
import { assert, getEnv } from '../../utils.js'
import { handleKnownTwitterErrors } from './utils.js'
/**
* This file contains rate-limited wrappers around all of the core Twitter API
* methods that this project uses.
*
* NOTE: Twitter has different API rate limits and quotas per plan, so in order
* to rate-limit effectively, our throttles need to either use the lowest common
* 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
*/
type TwitterApiMethod =
| 'createTweet'
| 'usersIdMentions'
| 'findTweetById'
| 'findTweetsById'
| 'findUserById'
| 'findUserByUsername'
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000
const FIFTEEN_MINUTES_MS = 15 * 60 * 1000
const twitterApiRateLimitsByPlan: Record<
types.TwitterApiPlan,
Record<
TwitterApiMethod,
{
readonly limit: number
readonly interval: number
}
>
> = {
free: {
// 50 per 24h per user
// 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 },
// TODO: according to the twitter docs, this shouldn't be allowed on the
// free plan, but it seems to work...
findTweetById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
// TODO: according to the twitter docs, this shouldn't be allowed on the
// free plan, but it seems to work...
findTweetsById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findUserById: { limit: 1, interval: FIFTEEN_MINUTES_MS },
findUserByUsername: { limit: 1, interval: FIFTEEN_MINUTES_MS }
},
basic: {
// 100 per 24h per user
// 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
// 180 per 15m per user
// 450 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 },
findUserById: { limit: 100, interval: TWENTY_FOUR_HOURS_MS },
findUserByUsername: { limit: 100, interval: TWENTY_FOUR_HOURS_MS }
},
pro: {
// 100 per 15m per user
// 10k per 24h per app
createTweet: { limit: 100, interval: FIFTEEN_MINUTES_MS },
// 180 per 15m per user
// 450 per 15m per app
usersIdMentions: { limit: 180, interval: FIFTEEN_MINUTES_MS },
// TODO: why would the per-user rate-limit be less 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 },
findUserById: { limit: 300, interval: FIFTEEN_MINUTES_MS },
findUserByUsername: { limit: 300, interval: FIFTEEN_MINUTES_MS }
},
enterprise: {
// NOTE: these are just placeholders; the enterprise plan seems to be
// 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 },
findTweetById: { limit: 4500, interval: FIFTEEN_MINUTES_MS },
findTweetsById: { limit: 4500, interval: FIFTEEN_MINUTES_MS },
findUserById: { limit: 3000, interval: FIFTEEN_MINUTES_MS },
findUserByUsername: { limit: 3000, interval: FIFTEEN_MINUTES_MS }
}
}
export class TwitterClient extends AIFunctionsProvider {
readonly client: types.TwitterV2Client
readonly twitterApiPlan: types.TwitterApiPlan
constructor({
client,
twitterApiPlan = (getEnv('TWITTER_API_PLAN') as types.TwitterApiPlan) ??
'free'
}: {
client: types.TwitterV2Client
twitterApiPlan?: types.TwitterApiPlan
}) {
assert(
client,
'TwitterClient missing required "client" which should be an instance of "twitter-api-sdk" (use `getTwitterV2Client` to initialize the underlying V2 Twitter SDK using Nango OAuth)'
)
assert(twitterApiPlan, 'TwitterClient missing required "twitterApiPlan"')
super()
this.client = client
this.twitterApiPlan = twitterApiPlan
const twitterApiRateLimits = twitterApiRateLimitsByPlan[twitterApiPlan]!
assert(twitterApiRateLimits, `Invalid twitter api plan: ${twitterApiPlan}`)
const createTweetThrottle = pThrottle(twitterApiRateLimits.createTweet)
const findTweetByIdThrottle = pThrottle(twitterApiRateLimits.findTweetById)
const findTweetsByIdThrottle = pThrottle(
twitterApiRateLimits.findTweetsById
)
const findUserByIdThrottle = pThrottle(twitterApiRateLimits.findUserById)
const findUserByUsernameThrottle = pThrottle(
twitterApiRateLimits.findUserByUsername
)
this._createTweet = createTweetThrottle(createTweetImpl(this.client))
this._findTweetById = findTweetByIdThrottle(findTweetByIdImpl(this.client))
this._findTweetsById = findTweetsByIdThrottle(
findTweetsByIdImpl(this.client)
)
this._findUserById = findUserByIdThrottle(findUserByIdImpl(this.client))
this._findUserByUsername = findUserByUsernameThrottle(
findUserByUsernameImpl(this.client)
)
}
protected _createTweet: ReturnType<typeof createTweetImpl>
protected _findTweetById: ReturnType<typeof findTweetByIdImpl>
protected _findTweetsById: ReturnType<typeof findTweetsByIdImpl>
protected _findUserById: ReturnType<typeof findUserByIdImpl>
protected _findUserByUsername: ReturnType<typeof findUserByUsernameImpl>
@aiFunction({
name: 'create_tweet',
description: 'Creates a new tweet',
inputSchema: z.object({
text: z.string().min(1)
})
})
async createTweet(
params: types.CreateTweetParams
): Promise<types.CreatedTweet> {
return this._createTweet(params)
}
@aiFunction({
name: 'get_tweet_by_id',
description: 'Fetch a tweet by its ID',
inputSchema: z.object({
id: z.string().min(1)
})
})
async findTweetById({
id,
...params
}: { id: string } & types.FindTweetByIdParams) {
assert(
this.twitterApiPlan !== 'free',
'TwitterClient.findTweetById not supported on free plan'
)
return this._findTweetById(id, params)
}
@aiFunction({
name: 'get_tweets_by_id',
description: 'Fetch an array of tweets by their IDs',
inputSchema: z.object({
ids: z.array(z.string().min(1))
})
})
async findTweetsById({ ids, ...params }: types.FindTweetsByIdParams) {
assert(
this.twitterApiPlan !== 'free',
'TwitterClient.findTweetsById not supported on free plan'
)
return this._findTweetsById(ids, params)
}
@aiFunction({
name: 'get_twitter_user_by_id',
description: 'Fetch a twitter user by ID',
inputSchema: z.object({
id: z.string().min(1)
})
})
async findUserById({
id,
...params
}: { id: string } & types.FindUserByIdParams) {
assert(
this.twitterApiPlan !== 'free',
'TwitterClient.findUserById not supported on free plan'
)
return this._findUserById(id, params)
}
@aiFunction({
name: 'get_twitter_user_by_username',
description: 'Fetch a twitter user by username',
inputSchema: z.object({
username: z.string().min(1)
})
})
async findUserByUsername({
username,
...params
}: { username: string } & types.FindUserByUsernameParams) {
assert(
this.twitterApiPlan !== 'free',
'TwitterClient.findUserByUsername not supported on free plan'
)
return this._findUserByUsername(username, params)
}
}
const defaultTwitterQueryTweetFields: types.TwitterQueryTweetFields = [
'attachments',
'author_id',
'conversation_id',
'created_at',
'entities',
'geo',
'id',
'in_reply_to_user_id',
'lang',
'public_metrics',
'possibly_sensitive',
'referenced_tweets',
'text'
// 'context_annotations', // not needed (way too verbose and noisy)
// 'edit_controls', / not needed
// 'non_public_metrics', // don't have access to
// 'organic_metrics', // don't have access to
// 'promoted_metrics, // don't have access to
// 'reply_settings', / not needed
// 'source', // not needed
// 'withheld' // not needed
]
const defaultTwitterQueryUserFields: types.TwitterQueryUserFields = [
'created_at',
'description',
'entities',
'id',
'location',
'name',
'pinned_tweet_id',
'profile_image_url',
'protected',
'public_metrics',
'url',
'username',
'verified'
// 'most_recent_tweet_id',
// 'verified_type',
// 'withheld'
]
const defaultTweetQueryParams: types.TweetsQueryOptions = {
// https://developer.twitter.com/en/docs/twitter-api/expansions
expansions: [
'author_id',
'in_reply_to_user_id',
'referenced_tweets.id',
'referenced_tweets.id.author_id',
'entities.mentions.username',
// TODO
'attachments.media_keys',
'geo.place_id',
'attachments.poll_ids'
],
'tweet.fields': defaultTwitterQueryTweetFields,
'user.fields': defaultTwitterQueryUserFields
}
const defaultUserQueryParams: types.TwitterUserQueryOptions = {
// https://developer.twitter.com/en/docs/twitter-api/expansions
expansions: ['pinned_tweet_id'],
'tweet.fields': defaultTwitterQueryTweetFields,
'user.fields': defaultTwitterQueryUserFields
}
function createTweetImpl(client: types.TwitterV2Client) {
return async (
params: types.CreateTweetParams
): Promise<types.CreatedTweet> => {
try {
const { data: tweet } = await client.tweets.createTweet(params)
if (!tweet?.id) {
throw new Error('invalid createTweet response')
}
return tweet
} catch (err: any) {
console.error('error creating tweet', JSON.stringify(err, null, 2))
handleKnownTwitterErrors(err, { label: 'creating tweet' })
throw err
}
}
}
function findTweetByIdImpl(client: types.TwitterV2Client) {
return async (tweetId: string, params?: types.FindTweetByIdParams) => {
try {
return await client.tweets.findTweetById(tweetId, {
...defaultTweetQueryParams,
...params
})
} catch (err: any) {
handleKnownTwitterErrors(err, { label: `fetching tweet ${tweetId}` })
throw err
}
}
}
function findTweetsByIdImpl(client: types.TwitterV2Client) {
return async (
ids: string[],
params?: Omit<types.FindTweetsByIdParams, 'ids'>
) => {
try {
return await client.tweets.findTweetsById({
...defaultTweetQueryParams,
...params,
ids
})
} catch (err: any) {
handleKnownTwitterErrors(err, { label: `fetching ${ids.length} tweets` })
throw err
}
}
}
function findUserByIdImpl(client: types.TwitterV2Client) {
return async (userId: string, params?: types.FindUserByIdParams) => {
try {
return await client.users.findUserById(userId, {
...defaultUserQueryParams,
...params
})
} catch (err: any) {
handleKnownTwitterErrors(err, {
label: `fetching user with id ${userId}`
})
throw err
}
}
}
function findUserByUsernameImpl(client: types.TwitterV2Client) {
return async (username: string, params?: types.FindUserByUsernameParams) => {
try {
return await client.users.findUserByUsername(username, {
...defaultUserQueryParams,
...params
})
} catch (err: any) {
handleKnownTwitterErrors(err, {
label: `fetching user with username ${username}`
})
throw err
}
}
}

Wyświetl plik

@ -0,0 +1,71 @@
import type { AsyncReturnType, Simplify } from 'type-fest'
import { type Client as TwitterV2Client } from 'twitter-api-sdk'
export { type Client as TwitterV2Client } from 'twitter-api-sdk'
export type TwitterApiPlan = 'free' | 'basic' | 'pro' | 'enterprise'
export type TweetsQueryOptions = Simplify<
Pick<
Parameters<TwitterV2Client['tweets']['findTweetsById']>[0],
'expansions' | 'tweet.fields' | 'user.fields'
>
>
export type TwitterUserQueryOptions = Simplify<
Pick<
NonNullable<Parameters<TwitterV2Client['users']['findUserById']>[1]>,
'expansions' | 'tweet.fields' | 'user.fields'
>
>
export type TwitterQueryTweetFields = TweetsQueryOptions['tweet.fields']
export type TwitterQueryUserFields = TweetsQueryOptions['user.fields']
export type TwitterUserIdMentionsQueryOptions = Simplify<
NonNullable<Parameters<TwitterV2Client['tweets']['usersIdMentions']>[1]>
>
export type CreateTweetParams = Simplify<
Parameters<TwitterV2Client['tweets']['createTweet']>[0]
>
export type UsersIdMentionsParams = Simplify<
Parameters<TwitterV2Client['tweets']['usersIdMentions']>[1]
>
export type FindTweetByIdParams = Simplify<
Parameters<TwitterV2Client['tweets']['findTweetById']>[1]
>
export type FindTweetsByIdParams = Simplify<
Parameters<TwitterV2Client['tweets']['findTweetsById']>[0]
>
export type FindUserByIdParams = Simplify<
Parameters<TwitterV2Client['users']['findUserById']>[1]
>
export type FindUserByUsernameParams = Simplify<
Parameters<TwitterV2Client['users']['findUserByUsername']>[1]
>
type Unpacked<T> = T extends (infer U)[] ? U : T
export type Tweet = Simplify<
NonNullable<
Unpacked<
AsyncReturnType<TwitterV2Client['tweets']['findTweetsById']>['data']
>
>
>
export type TwitterUser = Simplify<
NonNullable<AsyncReturnType<TwitterV2Client['users']['findMyUser']>['data']>
>
export type CreatedTweet = Simplify<
NonNullable<AsyncReturnType<TwitterV2Client['tweets']['createTweet']>['data']>
>
export type TwitterUrl = Simplify<
Unpacked<NonNullable<NonNullable<Tweet['entities']>['urls']>>
>

Wyświetl plik

@ -0,0 +1,140 @@
import type * as types from './types.js'
import { omit } from '../../utils.js'
import { TwitterError } from './error.js'
/**
* 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.
*/
export function handleKnownTwitterErrors(
err: any,
{ label = '' }: { label?: string } = {}
) {
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
}
)
} else if (err.status === 401) {
throw new TwitterError(`error ${label}: unauthorized`, {
type: 'twitter:auth',
cause: err
})
} else if (err.status === 400) {
if (
/value passed for the token was invalid/i.test(
err.error?.error_description
)
) {
throw new TwitterError(`error ${label}: invalid auth token`, {
type: 'twitter:auth',
cause: err
})
}
} else if (err.status === 429) {
throw new TwitterError(`error ${label}: too many requests`, {
type: 'twitter:rate-limit',
cause: err
})
} else if (err.status === 404) {
throw new TwitterError(err.toString(), {
type: 'twitter:forbidden',
isFinal: true,
cause: err
})
}
if (err.status >= 400 && err.status < 500) {
throw new TwitterError(
`error ${label}: ${err.status} ${
err.error?.description || err.toString()
}`,
{
type: 'twitter:unknown',
isFinal: true,
cause: err
}
)
} else if (err.status >= 500) {
throw new TwitterError(
`error ${label}: ${err.status} ${
err.error?.description || err.toString()
}`,
{
type: 'twitter:unknown',
isFinal: false,
cause: err
}
)
}
const reason = err.toString().toLowerCase()
if (reason.includes('fetcherror') || reason.includes('enotfound')) {
throw new TwitterError(err.toString(), {
type: 'network',
cause: err
})
}
// Otherwise, propagate the original error
throw err
}
export function getPrunedTweet(
tweet: Partial<types.Tweet>
): Partial<types.Tweet> {
const urls = tweet.entities?.urls
let text = tweet.text
if (text && urls) {
for (const url of urls) {
if (!url.expanded_url || !url.url) continue
text = text!.replaceAll(url.url, url.expanded_url!)
}
}
return {
...omit(
tweet,
'conversation_id',
'public_metrics',
'created_at',
'entities',
'possibly_sensitive'
),
text
}
}
export function getPrunedTwitterUser(
twitterUser: Partial<types.TwitterUser>
): Partial<types.TwitterUser> {
const urls = twitterUser.entities?.description?.urls
let description = twitterUser.description
if (description && urls) {
for (const url of urls) {
if (!url.expanded_url || !url.url) continue
description = description!.replaceAll(url.url, url.expanded_url!)
}
}
return {
...omit(
twitterUser,
'public_metrics',
'created_at',
'verified',
'protected',
'url',
'entities'
),
description
}
}

Wyświetl plik

@ -7,6 +7,9 @@ export default defineConfig([
'src/sdks/ai-sdk.ts',
'src/sdks/dexter.ts',
'src/sdks/genkit.ts',
'src/sdks/langchain.ts',
'src/sdks/llamaindex.ts',
'src/services/twitter/index.ts',
'src/tools/calculator.ts'
],
outDir: 'dist',