diff --git a/lib/config.ts b/lib/config.ts index df0721323..7403d58f4 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -204,10 +204,12 @@ type ConfigEnvKeys = | 'TUMBLR_CLIENT_ID' | 'TUMBLR_CLIENT_SECRET' | 'TUMBLR_REFRESH_TOKEN' - | 'TWITTER_USERNAME' - | 'TWITTER_PASSWORD' - | 'TWITTER_AUTHENTICATION_SECRET' - | 'TWITTER_PHONE_OR_EMAIL' + | 'TWITTER_CONSUMER_KEY' + | 'TWITTER_CONSUMER_SECRET' + // | 'TWITTER_USERNAME' + // | 'TWITTER_PASSWORD' + // | 'TWITTER_AUTHENTICATION_SECRET' + // | 'TWITTER_PHONE_OR_EMAIL' | 'TWITTER_AUTH_TOKEN' | 'TWITTER_THIRD_PARTY_API' | 'UESTC_BBS_COOKIE' @@ -617,10 +619,12 @@ export type Config = { refreshToken?: string; }; twitter: { - username?: string[]; - password?: string[]; - authenticationSecret?: string[]; - phoneOrEmail?: string[]; + consumerKey?: string; + consumerSecret?: string; + // username?: string[]; + // password?: string[]; + // authenticationSecret?: string[]; + // phoneOrEmail?: string[]; authToken?: string[]; thirdPartyApi?: string; }; @@ -1102,10 +1106,12 @@ const calculateValue = () => { refreshToken: envs.TUMBLR_REFRESH_TOKEN, }, twitter: { - username: envs.TWITTER_USERNAME?.split(','), - password: envs.TWITTER_PASSWORD?.split(','), - authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET?.split(','), - phoneOrEmail: envs.TWITTER_PHONE_OR_EMAIL?.split(','), + consumerKey: envs.TWITTER_CONSUMER_KEY, + consumerSecret: envs.TWITTER_CONSUMER_SECRET, + // username: envs.TWITTER_USERNAME?.split(','), + // password: envs.TWITTER_PASSWORD?.split(','), + // authenticationSecret: envs.TWITTER_AUTHENTICATION_SECRET?.split(','), + // phoneOrEmail: envs.TWITTER_PHONE_OR_EMAIL?.split(','), authToken: envs.TWITTER_AUTH_TOKEN?.split(','), thirdPartyApi: envs.TWITTER_THIRD_PARTY_API, }, diff --git a/lib/routes/twitter/api/developer-api/api.ts b/lib/routes/twitter/api/developer-api/api.ts new file mode 100644 index 000000000..1f2fcff79 --- /dev/null +++ b/lib/routes/twitter/api/developer-api/api.ts @@ -0,0 +1,330 @@ +import type { TwitterApiReadOnly } from 'twitter-api-v2'; +import { TwitterApi } from 'twitter-api-v2'; + +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import cache from '@/utils/cache'; + +const appClients: TwitterApiReadOnly[] = []; +let index = -1; + +const init = () => { + if (appClients.length) { + return; + } + if (!config.twitter.consumerKey || !config.twitter.consumerSecret) { + return; + } + + const consumerKeys = config.twitter.consumerKey.split(','); + const consumerSecrets = config.twitter.consumerSecret.split(','); + + for (const [index, consumerKey] of consumerKeys.entries()) { + const consumerSecret = consumerSecrets[index]; + if (!consumerKey || !consumerSecret) { + continue; + } + appClients.push( + new TwitterApi({ + appKey: consumerKey, + appSecret: consumerSecret, + }).readOnly + ); + } +}; + +export const getAppClient = async () => { + init(); + if (!appClients.length) { + throw new ConfigNotFoundError('Twitter API is not configured'); + } + index += 1; + return await appClients[index % appClients.length].appLogin(); +}; + +const mapUserToLegacy = (user: Record) => + user + ? { + id_str: user.id, + name: user.name, + screen_name: user.username, + description: user.description, + profile_image_url: user.profile_image_url, + profile_image_url_https: user.profile_image_url, + url: user.url, + verified: user.verified, + } + : null; + +const mapUrlsToLegacy = (urls: Array> = []) => + urls.map((url) => ({ + url: url.url, + expanded_url: url.expanded_url ?? url.unwound_url ?? url.url, + display_url: url.display_url ?? url.url, + })); + +const mapHashtagsToLegacy = (hashtags: Array> = []) => hashtags.map((hashtag) => ({ text: hashtag.tag })); + +const mapMentionsToLegacy = (mentions: Array> = []) => + mentions.map((mention) => ({ + id_str: mention.id, + screen_name: mention.username, + name: mention.username, + })); + +const mapMediaToLegacy = (media: Record) => { + const url = media.url ?? media.preview_image_url; + const mapped = { + id_str: media.media_key, + type: media.type, + media_url_https: url, + media_url: url, + url, + sizes: { + large: { + w: media.width ?? 0, + h: media.height ?? 0, + resize: 'fit', + }, + }, + } as Record; + + if (media.variants?.length) { + mapped.video_info = { + variants: media.variants.map((variant) => ({ + bitrate: variant.bit_rate, + content_type: variant.content_type, + url: variant.url, + })), + }; + } + + return mapped; +}; + +const mapTweetToLegacy = (tweet: Record, includes: Record | undefined, cacheMap: Map>) => { + if (cacheMap.has(tweet.id)) { + return cacheMap.get(tweet.id); + } + + const users = new Map((includes?.users ?? []).map((user) => [user.id, user])); + const tweets = new Map((includes?.tweets ?? []).map((item) => [item.id, item])); + const media = new Map((includes?.media ?? []).map((item) => [item.media_key, item])); + + const user = users.get(tweet.author_id); + const legacyUser = mapUserToLegacy(user); + const legacy: Record = { + id_str: tweet.id, + conversation_id_str: tweet.conversation_id, + full_text: tweet.text, + text: tweet.text, + created_at: tweet.created_at, + entities: { + urls: mapUrlsToLegacy(tweet.entities?.urls), + hashtags: mapHashtagsToLegacy(tweet.entities?.hashtags), + user_mentions: mapMentionsToLegacy(tweet.entities?.mentions), + symbols: [], + }, + extended_entities: { + media: (tweet.attachments?.media_keys ?? []) + .map((key) => media.get(key)) + .filter(Boolean) + .map((item) => mapMediaToLegacy(item)), + }, + user: legacyUser, + user_id_str: tweet.author_id, + in_reply_to_user_id_str: tweet.in_reply_to_user_id, + }; + + cacheMap.set(tweet.id, legacy); + + for (const reference of tweet.referenced_tweets ?? []) { + const referenced = tweets.get(reference.id); + if (!referenced) { + continue; + } + const mappedReferenced = mapTweetToLegacy(referenced, includes, cacheMap); + switch (reference.type) { + case 'retweeted': + legacy.retweeted_status = mappedReferenced; + break; + + case 'quoted': + legacy.quoted_status = mappedReferenced; + break; + + case 'replied_to': { + legacy.in_reply_to_status_id_str = reference.id; + legacy.in_reply_to_user_id_str = referenced.author_id; + const replyUser = users.get(referenced.author_id); + legacy.in_reply_to_screen_name = replyUser?.username; + + break; + } + default: + // Do nothing + } + } + + if (!legacy.extended_entities.media?.length) { + delete legacy.extended_entities; + } + + return legacy; +}; + +const mapTweetResponseToLegacy = (response: Record) => { + const cacheMap = new Map>(); + return (response?.data ?? []).map((tweet) => mapTweetToLegacy(tweet, response.includes, cacheMap)); +}; + +const getUserData = (id: string) => + cache.tryGet(`twitter-userdata-${id}`, async () => { + const client = await getAppClient(); + const params = { + 'user.fields': 'profile_image_url,description,verified,url', + }; + const response = id.startsWith('+') ? await client.v2.user(id.slice(1), params) : await client.v2.userByUsername(id, params); + return mapUserToLegacy(response?.data); + }); + +const cacheTryGet = async (_id: string, params: Record | undefined, func: (id: string, params?: Record) => Promise) => { + const userData: any = await getUserData(_id); + const id = userData?.id_str; + if (id === undefined) { + cache.set(`twitter-userdata-${_id}`, '', config.cache.contentExpire); + throw new InvalidParameterError('User not found'); + } + const funcName = func.name; + const paramsString = JSON.stringify(params); + return cache.tryGet(`twitter:${id}:${funcName}:${paramsString}`, () => func(id, params), config.cache.routeExpire, false); +}; + +const getUserTimeline = async (id: string, params?: Record, options: Record = {}) => { + const client = await getAppClient(); + const response = await client.v2.get(`users/${id}/tweets`, { + max_results: params?.count ?? 20, + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + ...options, + }); + return mapTweetResponseToLegacy(response); +}; + +const getUserTweets = (id: string, params?: Record) => cacheTryGet(id, params, (id, params = {}) => getUserTimeline(id, params, { exclude: 'replies' })); + +const getUserTweetsAndReplies = (id: string, params?: Record) => cacheTryGet(id, params, (id, params = {}) => getUserTimeline(id, params)); + +const getUserMedia = (id: string, params?: Record) => + cacheTryGet(id, params, async (id, params = {}) => { + const data = await getUserTimeline(id, params); + return data.filter((tweet) => tweet.extended_entities?.media); + }); + +const getUserLikes = (id: string, params?: Record) => + cacheTryGet(id, params, async (id, params = {}) => { + const client = await getAppClient(); + const response = await client.v2.get(`users/${id}/liked_tweets`, { + max_results: params.count ?? 20, + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + }); + return mapTweetResponseToLegacy(response); + }); + +const getUserTweet = (id: string, params?: Record) => + cacheTryGet(id, params, async (_id, params = {}) => { + const client = await getAppClient(); + const tweetId = params.focalTweetId; + if (!tweetId) { + throw new InvalidParameterError('Tweet ID is required'); + } + const response = await client.v2.get(`tweets/${tweetId}`, { + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + }); + return mapTweetResponseToLegacy({ data: response?.data ? [response.data] : [], includes: response?.includes }); + }); + +const getSearch = (keywords: string, params?: Record) => + cache.tryGet( + `twitter:search:${keywords}:${JSON.stringify(params)}`, + async () => { + const client = await getAppClient(); + const response = await client.v2.get('tweets/search/recent', { + query: keywords, + max_results: params?.count ?? 20, + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + }); + return mapTweetResponseToLegacy(response); + }, + config.cache.routeExpire, + false + ); + +const getList = (id: string, params?: Record) => + cache.tryGet( + `twitter:list:${id}:${JSON.stringify(params)}`, + async () => { + const client = await getAppClient(); + const response = await client.v2.get(`lists/${id}/tweets`, { + max_results: params?.count ?? 20, + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + }); + return mapTweetResponseToLegacy(response); + }, + config.cache.routeExpire, + false + ); + +const getHomeTimeline = (_id: string, params?: Record) => + cache.tryGet( + `twitter:home:${JSON.stringify(params)}`, + async () => { + if (!_id) { + throw new InvalidParameterError('User ID is required for the v2 home timeline'); + } + const client = await getAppClient(); + const response = await client.v2.get(`users/${_id}/timelines/reverse_chronological`, { + max_results: params?.count ?? 20, + expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id', + 'tweet.fields': 'created_at,entities,conversation_id,referenced_tweets,author_id,in_reply_to_user_id', + 'user.fields': 'username,name,profile_image_url,description', + 'media.fields': 'preview_image_url,url,type,width,height,variants', + }); + return mapTweetResponseToLegacy(response); + }, + config.cache.routeExpire, + false + ); + +const getHomeLatestTimeline = (id: string, params?: Record) => getHomeTimeline(id, params); + +const getUser = (id: string) => getUserData(id); + +export default { + getUser, + getUserTweets, + getUserTweetsAndReplies, + getUserMedia, + getUserLikes, + getUserTweet, + getSearch, + getList, + getHomeTimeline, + getHomeLatestTimeline, + init, +}; diff --git a/lib/routes/twitter/api/developer-api/search.ts b/lib/routes/twitter/api/developer-api/search.ts index 484b93cd1..eb5c096f1 100644 --- a/lib/routes/twitter/api/developer-api/search.ts +++ b/lib/routes/twitter/api/developer-api/search.ts @@ -1,21 +1,17 @@ import utils from '../../utils'; +import api from './api'; const handler = async (ctx) => { const keyword = ctx.req.param('keyword'); const limit = ctx.req.query('limit') ?? 50; - const client = await utils.getAppClient(); - const data = await client.v1.get('search/tweets.json', { - q: keyword, - count: limit, - tweet_mode: 'extended', - result_type: 'recent', - }); + await api.init(); + const data = await api.getSearch(keyword, { count: limit }); return { title: `Twitter Keyword - ${keyword}`, link: `https://x.com/search?q=${encodeURIComponent(keyword)}`, item: utils.ProcessFeed(ctx, { - data: data.statuses, + data, }), allowEmpty: true, }; diff --git a/lib/routes/twitter/api/developer-api/user.ts b/lib/routes/twitter/api/developer-api/user.ts index 29ba86c18..26e52b370 100644 --- a/lib/routes/twitter/api/developer-api/user.ts +++ b/lib/routes/twitter/api/developer-api/user.ts @@ -1,38 +1,31 @@ import utils from '../../utils'; +import api from './api'; const handler = async (ctx) => { const id = ctx.req.param('id'); // For compatibility const { include_replies, include_rts, count } = utils.parseRouteParams(ctx.req.param('routeParams')); - const client = await utils.getAppClient(); - const user_timeline_query = { - tweet_mode: 'extended', - exclude_replies: !include_replies, - include_rts, - count, - }; - let screen_name; - if (id.startsWith('+')) { - user_timeline_query.user_id = +id.slice(1); - } else { - user_timeline_query.screen_name = id; - screen_name = id; + const params = count ? { count } : {}; + + await api.init(); + const userInfo: any = await api.getUser(id); + let data = await (include_replies ? api.getUserTweetsAndReplies(id, params) : api.getUserTweets(id, params)); + if (!include_rts) { + data = utils.excludeRetweet(data); } - const data = await client.v1.get('statuses/user_timeline.json', user_timeline_query); - const userInfo = data[0].user; - if (!screen_name) { - screen_name = userInfo.screen_name; - } - const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https; + + const screenName = userInfo?.screen_name; + const profileImageUrl = userInfo?.profile_image_url || userInfo?.profile_image_url_https; return { title: `Twitter @${userInfo.name}`, - link: `https://x.com/${screen_name}`, + link: `https://x.com/${screenName}`, image: profileImageUrl, description: userInfo.description, item: utils.ProcessFeed(ctx, { data, }), + allowEmpty: true, }; }; export default handler; diff --git a/lib/routes/twitter/api/index.ts b/lib/routes/twitter/api/index.ts index 59536b138..2e8151c4e 100644 --- a/lib/routes/twitter/api/index.ts +++ b/lib/routes/twitter/api/index.ts @@ -1,12 +1,14 @@ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; -import mobileApi from './mobile-api/api'; +import devApi from './developer-api/api'; +// import mobileApi from './mobile-api/api'; import webApi from './web-api/api'; const enableThirdPartyApi = config.twitter.thirdPartyApi; -const enableMobileApi = config.twitter.username && config.twitter.password; +// const enableMobileApi = config.twitter.username && config.twitter.password; const enableWebApi = config.twitter.authToken; +const enableDeveloperApi = config.twitter.consumerKey && config.twitter.consumerSecret; type ApiItem = (id: string, params?: Record) => Promise> | Record | null; let api: { @@ -41,8 +43,8 @@ if (enableThirdPartyApi) { api = webApi; } else if (enableWebApi) { api = webApi; -} else if (enableMobileApi) { - api = mobileApi; +} else if (enableDeveloperApi) { + api = devApi; } export default api; diff --git a/lib/routes/twitter/home-latest.ts b/lib/routes/twitter/home-latest.ts index df4088944..a9d4e9abe 100644 --- a/lib/routes/twitter/home-latest.ts +++ b/lib/routes/twitter/home-latest.ts @@ -9,14 +9,14 @@ export const route: Route = { example: '/twitter/home_latest', features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, { name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', diff --git a/lib/routes/twitter/home.ts b/lib/routes/twitter/home.ts index 64a8d103c..4879e767e 100644 --- a/lib/routes/twitter/home.ts +++ b/lib/routes/twitter/home.ts @@ -9,14 +9,14 @@ export const route: Route = { example: '/twitter/home', features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, { name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', diff --git a/lib/routes/twitter/keyword.ts b/lib/routes/twitter/keyword.ts index 7756ee061..40ab7438b 100644 --- a/lib/routes/twitter/keyword.ts +++ b/lib/routes/twitter/keyword.ts @@ -12,14 +12,14 @@ export const route: Route = { parameters: { keyword: 'keyword', routeParams: 'extra parameters, see the table above' }, features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, { name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', diff --git a/lib/routes/twitter/media.ts b/lib/routes/twitter/media.ts index 6c5a6569e..5dccf2d05 100644 --- a/lib/routes/twitter/media.ts +++ b/lib/routes/twitter/media.ts @@ -13,14 +13,14 @@ export const route: Route = { parameters: { id: 'username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', routeParams: 'extra parameters, see the table above.' }, features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, { name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', diff --git a/lib/routes/twitter/namespace.ts b/lib/routes/twitter/namespace.ts index f3662fbbf..acdba9a8d 100644 --- a/lib/routes/twitter/namespace.ts +++ b/lib/routes/twitter/namespace.ts @@ -45,7 +45,10 @@ Currently supports two authentication methods: - Using \`TWITTER_AUTH_TOKEN\` (recommended): Configure a comma-separated list of \`auth_token\` cookies of logged-in Twitter Web. RSSHub will use this information to directly access Twitter's web API to obtain data. -- Using \`TWITTER_USERNAME\` \`TWITTER_PASSWORD\` and \`TWITTER_AUTHENTICATION_SECRET\`: Configure a comma-separated list of Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism. +~~- Using \`TWITTER_USERNAME\` \`TWITTER_PASSWORD\` and \`TWITTER_AUTHENTICATION_SECRET\`: Configure a comma-separated list of Twitter username and password. RSSHub will use this information to log in to Twitter and obtain data using the mobile API. Please note that if you have not logged in with the current IP address before, it is easy to trigger Twitter's risk control mechanism.~~ This no longer works since mobile client attestation has been implemented in October 2025. + +- Using \`TWITTER_CONSUMER_KEY\` and \`TWITTER_CONSUMER_SECRET\`: Configure a comma-separated list of Twitter API keys and secrets. RSSHub will use this information to access Twitter's Pay-Per-Use developer API to obtain data. + `, lang: 'en', }; diff --git a/lib/routes/twitter/trends.ts b/lib/routes/twitter/trends.ts index 8b0e63c0c..214082213 100644 --- a/lib/routes/twitter/trends.ts +++ b/lib/routes/twitter/trends.ts @@ -2,7 +2,7 @@ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; import type { Route } from '@/types'; -import utils from './utils'; +import { getAppClient } from './api/developer-api/api'; export const route: Route = { path: '/trends/:woeid?', @@ -23,11 +23,11 @@ export const route: Route = { }; async function handler(ctx) { - if (!config.twitter || !config.twitter.consumer_key || !config.twitter.consumer_secret) { + if (!config.twitter || !config.twitter.consumerKey || !config.twitter.consumerSecret) { throw new ConfigNotFoundError('Twitter RSS is disabled due to the lack of relevant config'); } const woeid = ctx.req.param('woeid') ?? 1; // Global information is available by using 1 as the WOEID - const client = await utils.getAppClient(); + const client = await getAppClient(); const data = await client.v1.get('trends/place.json', { id: woeid }); const [{ trends }] = data; diff --git a/lib/routes/twitter/tweet.ts b/lib/routes/twitter/tweet.ts index 02470cf5f..f50fec964 100644 --- a/lib/routes/twitter/tweet.ts +++ b/lib/routes/twitter/tweet.ts @@ -16,14 +16,14 @@ export const route: Route = { }, features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, ], requirePuppeteer: false, antiCrawler: false, diff --git a/lib/routes/twitter/user.ts b/lib/routes/twitter/user.ts index 452fa6674..df4d0ac38 100644 --- a/lib/routes/twitter/user.ts +++ b/lib/routes/twitter/user.ts @@ -16,19 +16,19 @@ export const route: Route = { }, features: { requireConfig: [ - { - name: 'TWITTER_USERNAME', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_PASSWORD', - description: 'Please see above for details.', - }, - { - name: 'TWITTER_AUTHENTICATION_SECRET', - description: 'TOTP 2FA secret, please see above for details.', - optional: true, - }, + // { + // name: 'TWITTER_USERNAME', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_PASSWORD', + // description: 'Please see above for details.', + // }, + // { + // name: 'TWITTER_AUTHENTICATION_SECRET', + // description: 'TOTP 2FA secret, please see above for details.', + // optional: true, + // }, { name: 'TWITTER_AUTH_TOKEN', description: 'Please see above for details.', @@ -38,6 +38,14 @@ export const route: Route = { description: 'Use third-party API to query twitter data', optional: true, }, + { + name: 'TWITTER_CONSUMER_KEY', + description: 'Please see above for details.', + }, + { + name: 'TWITTER_CONSUMER_SECRET', + description: 'Please see above for details.', + }, ], requirePuppeteer: false, antiCrawler: false, diff --git a/lib/routes/twitter/utils.ts b/lib/routes/twitter/utils.ts index 48ef1ae39..179a1458c 100644 --- a/lib/routes/twitter/utils.ts +++ b/lib/routes/twitter/utils.ts @@ -1,6 +1,3 @@ -import { TwitterApi } from 'twitter-api-v2'; - -import { config } from '@/config'; import { parseDate } from '@/utils/parse-date'; import { fallback, queryToBoolean, queryToInteger } from '@/utils/readable-social'; @@ -440,32 +437,6 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => { }); }; -let getAppClient = () => null; - -if (config.twitter.consumer_key && config.twitter.consumer_secret) { - const consumer_keys = config.twitter.consumer_key.split(','); - const consumer_secrets = config.twitter.consumer_secret.split(','); - const T = {}; - let count = 0; - let index = -1; - - for (const [i, consumer_key] of consumer_keys.entries()) { - const consumer_secret = consumer_secrets[i]; - if (consumer_key && consumer_secret) { - T[i] = new TwitterApi({ - appKey: consumer_key, - appSecret: consumer_secret, - }).readOnly; - count = i + 1; - } - } - - getAppClient = () => { - index++; - return T[index % count].appLogin(); - }; -} - const parseRouteParams = (routeParams) => { let count, include_replies, include_rts, only_media; let force_web_api = false; @@ -519,7 +490,6 @@ export const keepOnlyMedia = function (tweets) { export default { ProcessFeed, - getAppClient, parseRouteParams, excludeRetweet, keepOnlyMedia,