import type {
JsonRecord,
ResolvedXOAuth,
XApiResponse,
XConnectUrls,
XRateLimitInfo,
XTweet,
XUser,
XUserSummary,
} from './types.ts'
export const API_BASE_URL = 'https://api.x.com'
export const DEFAULT_INTEGRATION = 'x'
export const BEARER_SECRET_NAME = 'xBearerToken'
export const DEFAULT_SCOPES = [
'tweet.read',
'tweet.write',
'users.read',
'offline.access',
'like.read',
'like.write',
'follows.read',
'follows.write',
'bookmark.read',
'bookmark.write',
] as const
export function connectUrls(integrationName = DEFAULT_INTEGRATION): XConnectUrls {
const provider = encodeURIComponent(integrationName)
const firstTime = new URL('https://kody.codes/connect/oauth')
firstTime.searchParams.set('provider', integrationName)
firstTime.searchParams.set('authorizeUrl', 'https://x.com/i/oauth2/authorize')
firstTime.searchParams.set('tokenUrl', 'https://api.x.com/2/oauth2/token')
firstTime.searchParams.set('apiBaseUrl', 'https://api.x.com/2')
firstTime.searchParams.set('flow', 'pkce')
firstTime.searchParams.set('allowedHosts', 'api.x.com,upload.x.com')
firstTime.searchParams.set('dashboardUrl', 'https://developer.x.com/en/portal/dashboard')
firstTime.searchParams.set('scopes', DEFAULT_SCOPES.join(' '))
const bearer = new URL('https://kody.codes/account/secrets/new')
bearer.searchParams.set('name', BEARER_SECRET_NAME)
bearer.searchParams.set(
'description',
'X API v2 app-only bearer token for optional public reads',
)
bearer.searchParams.set('allowedHosts', 'api.x.com')
bearer.searchParams.set('scope', 'user')
return {
reconnect: `https://kody.codes/connect/oauth?provider=${provider}`,
firstTime: firstTime.toString(),
bearerSecret: bearer.toString(),
}
}
export function cleanObject(input: JsonRecord): JsonRecord {
return Object.fromEntries(
Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ''),
)
}
export function extractRateLimit(response: Response): XRateLimitInfo {
return {
limit: response.headers.get('x-rate-limit-limit'),
remaining: response.headers.get('x-rate-limit-remaining'),
reset: response.headers.get('x-rate-limit-reset'),
}
}
export function summarizeUser(user: XUser | null | undefined): XUserSummary | null {
if (!user || typeof user !== 'object') return null
return {
id: typeof user.id === 'string' ? user.id : undefined,
username: typeof user.username === 'string' ? user.username : undefined,
name: typeof user.name === 'string' ? user.name : undefined,
}
}
export function shapeTweet(tweet: XTweet): JsonRecord {
return {
id: tweet.id,
text: tweet.text,
author_id: tweet.author_id,
created_at: tweet.created_at,
conversation_id: tweet.conversation_id,
public_metrics: tweet.public_metrics,
referenced_tweets: tweet.referenced_tweets,
}
}
export function shapeTweetsResponse(payload: XApiResponse<XTweet[] | XTweet>): JsonRecord {
const data = payload.data
const shaped = Array.isArray(data)
? data.map((tweet) => shapeTweet(tweet))
: data
? shapeTweet(data)
: data
return {
data: shaped,
includes: payload.includes,
meta: payload.meta,
errors: payload.errors,
rateLimit: payload.rateLimit,
}
}
export function defaultUserFields(fields?: string): string {
return fields || 'id,name,username,verified,public_metrics,description,created_at'
}
export function defaultPublicUserFields(fields?: string): string {
return fields || 'id,name,username,verified,public_metrics,description,created_at,url,location'
}
export function defaultTweetFields(fields?: string): string {
return (
fields ||
'id,text,author_id,created_at,conversation_id,public_metrics,referenced_tweets,entities'
)
}
export function defaultSearchExpansions(expansions?: string): string {
return expansions || 'author_id,referenced_tweets.id'
}
export function defaultSearchUserFields(fields?: string): string {
return fields || 'id,name,username,verified,public_metrics'
}
export function oauthTokenStorageNotes(
oauth?: Pick<
ResolvedXOAuth,
'integrationName' | 'account' | 'accessTokenSecretName' | 'refreshTokenSecretName' | 'clientId'
>,
) {
const integrationName = oauth?.integrationName || DEFAULT_INTEGRATION
const accessToken = oauth?.accessTokenSecretName || 'xAccessToken'
const refreshToken = oauth?.refreshTokenSecretName || 'xRefreshToken'
return {
integration: integrationName,
account: oauth?.account ?? null,
secrets: {
accessToken,
refreshToken,
optionalBearer: BEARER_SECRET_NAME,
},
clientId: oauth?.clientId ?? null,
refreshEndpoint: 'https://api.x.com/2/oauth2/token',
connect: connectUrls(integrationName),
notes: [
`User-context reads and writes use the saved "${integrationName}" OAuth integration.`,
`Pass account: '<purpose>' for integration x-<purpose>, or integration: '${integrationName}'.`,
`refresh-oauth-token rotates ${accessToken} through the host refresh helper.`,
`Optional app-only public reads can use ${BEARER_SECRET_NAME} with authMode: 'bearer'.`,
],
}
}