UNPKG

rsshub

Version:
1,021 lines (1,011 loc) • 42.7 kB
import { t as config } from "./config-C37vj7VH.mjs"; import { t as logger_default } from "./logger-Czu8UMNd.mjs"; import { t as ofetch_default } from "./ofetch-BIyrKU3Y.mjs"; import { t as cache_default } from "./cache-Bo__VnGm.mjs"; import { t as proxy_default } from "./proxy-Db7uGcYb.mjs"; import { t as got_default } from "./got-KxxWdaxq.mjs"; import { t as invalid_parameter_default } from "./invalid-parameter-rr4AgGpp.mjs"; import { t as config_not_found_default } from "./config-not-found-Dyp3RlZZ.mjs"; import { n as puppeteer_default } from "./puppeteer-DGmvuGvT.mjs"; import { Client, ProxyAgent } from "undici"; import crypto from "node:crypto"; import { RateLimiterMemory, RateLimiterQueue, RateLimiterRedis } from "rate-limiter-flexible"; import CryptoJS from "crypto-js"; import { Cookie, CookieJar } from "tough-cookie"; import queryString from "query-string"; import OAuth from "oauth-1.0a"; import { authenticator } from "otplib"; import { v5 } from "uuid"; import { CookieAgent, cookie } from "http-cookie-agent/undici"; //#region lib/routes/twitter/api/mobile-api/constants.ts const baseUrl$1 = "https://api.x.com"; const consumerKey = "3nVuSoBZnx6U4vzUxf5w"; const consumerSecret = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"; const gqlMap$1 = Object.fromEntries([ "/graphql/u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery", "/graphql/oPppcargziU1uDQHAUmH-A/UserResultByIdQuery", "/graphql/3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2", "/graphql/8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2", "/graphql/PDfFf8hGeJvUCiTyWtw4wQ/MediaTimelineV2", "/graphql/q94uRCEn65LZThakYcPT6g/TweetDetail", "/graphql/sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery", "/graphql/gkjsKepM6gl_HmFWoWKfgg/SearchTimeline", "/graphql/iTpgCtbdxrsJfyx0cFjHqg/ListByRestId", "/graphql/-kmqNvm5Y-cVrfvBy6docg/ListBySlug", "/graphql/P4NpVZDqUD_7MEM84L-8nw/ListMembers", "/graphql/BbGLL1ZfMibdFNWlk7a0Pw/ListTimeline" ].map((endpoint) => [endpoint.split("/")[3].replace(/V2$|Query$|QueryV2$/, ""), endpoint])); const gqlFeatures$1 = JSON.stringify({ android_graphql_skip_api_media_color_palette: false, blue_business_profile_image_shape_enabled: false, creator_subscriptions_subscription_count_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, freedom_of_speech_not_reach_fetch_enabled: false, graphql_is_translatable_rweb_tweet_is_translatable_enabled: false, hidden_profile_likes_enabled: false, highlights_tweets_tab_ui_enabled: false, interactive_text_enabled: false, longform_notetweets_consumption_enabled: true, longform_notetweets_inline_media_enabled: false, longform_notetweets_richtext_consumption_enabled: true, longform_notetweets_rich_text_read_enabled: false, responsive_web_edit_tweet_api_enabled: false, responsive_web_enhance_cards_enabled: false, responsive_web_graphql_exclude_directive_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: false, responsive_web_media_download_video_enabled: false, responsive_web_text_conversations_enabled: false, responsive_web_twitter_article_tweet_consumption_enabled: false, responsive_web_twitter_blue_verified_badge_is_enabled: true, rweb_lists_timeline_redesign_enabled: true, spaces_2022_h2_clipping: true, spaces_2022_h2_spaces_communities: true, standardized_nudges_misinfo: false, subscriptions_verification_info_enabled: true, subscriptions_verification_info_reason_enabled: true, subscriptions_verification_info_verified_since_enabled: true, super_follow_badge_privacy_enabled: false, super_follow_exclusive_tweet_notifications_enabled: false, super_follow_tweet_api_enabled: false, super_follow_user_api_enabled: false, tweet_awards_web_tipping_enabled: false, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: false, tweetypie_unmention_optimization_enabled: false, unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false, verified_phone_label_enabled: false, vibe_api_enabled: false, view_counts_everywhere_api_enabled: false }); const bearerToken$1 = "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"; const guestActivateUrl = baseUrl$1 + "/1.1/guest/activate.json"; //#endregion //#region lib/routes/twitter/api/mobile-api/login.ts const ENDPOINT = "https://api.x.com/1.1/onboarding/task.json"; const NAMESPACE = "d41d092b-b007-48f7-9129-e9538d2d8fe9"; const headers = { "User-Agent": "TwitterAndroid/10.21.0-release.0 (310210000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)", "X-Twitter-API-Version": "5", "X-Twitter-Client": "TwitterAndroid", "X-Twitter-Client-Version": "10.21.0-release.0", "OS-Version": "28", "System-User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)", "X-Twitter-Active-User": "yes", "Content-Type": "application/json", Authorization: bearerToken$1 }; const loginLimiterQueue$1 = new RateLimiterQueue(cache_default.clients.redisClient ? new RateLimiterRedis({ points: 1, duration: 20, execEvenly: true, storeClient: cache_default.clients.redisClient }) : new RateLimiterMemory({ points: 1, duration: 20, execEvenly: true })); const postTask = async (flowToken, subtaskId, subtaskInput) => await got_default.post(ENDPOINT, { headers, json: { flow_token: flowToken, subtask_inputs: [Object.assign({ subtask_id: subtaskId }, subtaskInput)] } }); const flowTasks = { async LoginEnterUserIdentifier({ flowToken, username }) { return await postTask(flowToken, "LoginEnterUserIdentifier", { enter_text: { suggestion_id: null, text: username, link: "next_link" } }); }, async LoginEnterPassword({ flowToken, password }) { return await postTask(flowToken, "LoginEnterPassword", { enter_password: { password, link: "next_link" } }); }, async LoginEnterAlternateIdentifierSubtask({ flowToken, phoneOrEmail }) { return await postTask(flowToken, "LoginEnterAlternateIdentifierSubtask", { enter_text: { suggestion_id: null, text: phoneOrEmail, link: "next_link" } }); }, async AccountDuplicationCheck({ flowToken }) { return await postTask(flowToken, "AccountDuplicationCheck", { check_logged_in_account: { link: "AccountDuplicationCheck_false" } }); }, async LoginTwoFactorAuthChallenge({ flowToken, authenticationSecret }) { return await postTask(flowToken, "LoginTwoFactorAuthChallenge", { enter_text: { suggestion_id: null, text: authenticator.generate(authenticationSecret), link: "next_link" } }); } }; async function login$1({ username, password, authenticationSecret, phoneOrEmail }) { return await cache_default.tryGet(`twitter:authentication:${username}`, async () => { try { await loginLimiterQueue$1.removeTokens(1); logger_default.debug("Twitter login start."); headers["X-Twitter-Client-DeviceID"] = v5(username, NAMESPACE); const ct0 = crypto.randomUUID().replaceAll("-", ""); const guestToken = await got_default(guestActivateUrl, { headers: { authorization: bearerToken$1, "x-csrf-token": ct0, cookie: "ct0=" + ct0 }, method: "POST" }); logger_default.debug("Twitter login: guest token"); headers["x-guest-token"] = guestToken.data.guest_token; let task = await ofetch_default.raw(ENDPOINT + "?" + new URLSearchParams({ flow_name: "login", api_version: "1", known_device_token: "", sim_country_code: "us" }).toString(), { method: "POST", headers, body: { flow_token: null, input_flow_data: { country_code: null, flow_context: { referrer_context: { referral_details: "utm_source=google-play&utm_medium=organic", referrer_url: "" }, start_location: { location: "deeplink" } }, requested_variant: null, target_user_id: 0 } } }).then(({ headers: _headers, _data }) => { headers.att = _headers.get("att"); return { data: _data }; }); logger_default.debug("Twitter login flow start."); const runTask = async ({ data }) => { const { subtask_id, open_account } = data.subtasks.shift(); if (open_account) return open_account; if (!(subtask_id in flowTasks)) { logger_default.error(`Twitter login flow task failed: unknown subtask: ${subtask_id}`); return; } task = await flowTasks[subtask_id]({ flowToken: data.flow_token, username, password, authenticationSecret, phoneOrEmail }); logger_default.debug(`Twitter login flow task finished: subtask: ${subtask_id}.`); return await runTask(task); }; const authentication = await runTask(task); logger_default.debug("Twitter login flow finished."); if (authentication) logger_default.debug("Twitter login success.", authentication); else logger_default.error(`Twitter login failed. ${JSON.stringify(task.data?.subtasks, null, 2)}`); return authentication; } catch (error) { logger_default.error(`Twitter username ${username} login failed:`, error); } }, 3600 * 24 * 30, false); } var login_default$1 = login$1; //#endregion //#region lib/routes/twitter/api/mobile-api/token.ts let tokenIndex = 0; async function getToken() { let token; if (config.twitter.username && config.twitter.password) { const index = tokenIndex++ % config.twitter.username.length; const username = config.twitter.username[index]; const password = config.twitter.password[index]; const authenticationSecret = config.twitter.authenticationSecret?.[index]; const phoneOrEmail = config.twitter.phoneOrEmail?.[index]; if (username && password) { const authentication = await login_default$1({ username, password, authenticationSecret, phoneOrEmail }); if (!authentication) throw new config_not_found_default(`Invalid twitter configs: ${username}`); token = { key: authentication.oauth_token, secret: authentication.oauth_token_secret, cacheKey: `twitter:authentication:${username}` }; } } else throw new config_not_found_default("Invalid twitter configs"); return token; } //#endregion //#region lib/routes/twitter/api/mobile-api/api.ts const twitterGot$1 = async (url, params) => { const token = await getToken(); const oauth = new OAuth({ consumer: { key: consumerKey, secret: consumerSecret }, signature_method: "HMAC-SHA1", hash_function: (base_string, key) => CryptoJS.HmacSHA1(base_string, key).toString(CryptoJS.enc.Base64) }); const requestData = { url: `${url}?${queryString.stringify(params)}`, method: "GET", headers: { connection: "keep-alive", "content-type": "application/json", "x-twitter-active-user": "yes", authority: "api.x.com", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", accept: "*/*", DNT: "1" } }; const response = await ofetch_default.raw(requestData.url, { headers: oauth.toHeader(oauth.authorize(requestData, token)) }); if (response.status === 401) cache_default.globalCache.set(token.cacheKey, ""); return response._data; }; const paginationTweets$1 = async (endpoint, userId, variables, path) => { const { data } = await twitterGot$1(baseUrl$1 + endpoint, { variables: JSON.stringify({ ...variables, rest_id: userId }), features: gqlFeatures$1 }); let instructions; if (path) { instructions = data; for (const p of path) instructions = instructions[p]; instructions = instructions.instructions; } else instructions = data.user_result.result.timeline_response.timeline.instructions; return instructions.find((i) => i.__typename === "TimelineAddEntries" || i.type === "TimelineAddEntries").entries; }; const timelineTweets = (userId, params = {}) => paginationTweets$1(gqlMap$1.UserWithProfileTweets, userId, { ...params, withQuickPromoteEligibilityTweetFields: true }); const timelineTweetsAndReplies = (userId, params = {}) => paginationTweets$1(gqlMap$1.UserWithProfileTweetsAndReplies, userId, { ...params, count: 20 }); const timelineMedia = (userId, params = {}) => paginationTweets$1(gqlMap$1.MediaTimeline, userId, params); const timelineKeywords = (keywords, params = {}) => paginationTweets$1(gqlMap$1.SearchTimeline, null, { ...params, rawQuery: keywords, count: 20, product: "Latest", withDownvotePerspective: false, withReactionsMetadata: false, withReactionsPerspective: false }, [ "search_by_raw_query", "search_timeline", "timeline" ]); const tweetDetail = (userId, params) => paginationTweets$1(gqlMap$1.TweetDetail, userId, { ...params, includeHasBirdwatchNotes: false, includePromotedContent: false, withBirdwatchNotes: false, withVoice: false, withV2Timeline: true }, ["threaded_conversation_with_injections_v2"]); const listTweets = (listId, params = {}) => paginationTweets$1(gqlMap$1.ListTimeline, listId, { ...params }, [ "list", "timeline_response", "timeline" ]); function gatherLegacyFromData$1(entries, filterNested, userId) { const tweets = []; const filteredEntries = []; for (const entry of entries) { const entryId = entry.entryId; if (entryId) { if (entryId.startsWith("tweet-")) filteredEntries.push(entry); if (filterNested && filterNested.some((f) => entryId.startsWith(f))) filteredEntries.push(...entry.content.items); } } for (const entry of filteredEntries) if (entry.entryId) { const content = entry.content || entry.item; let tweet = content?.content?.tweetResult?.result || content?.itemContent?.tweet_results?.result; if (tweet && tweet.tweet) tweet = tweet.tweet; if (tweet) { const retweet = tweet.legacy?.retweeted_status_result?.result; for (const t of [tweet, retweet]) { if (!t?.legacy) continue; t.legacy.user = t.core?.user_result?.result?.legacy || t.core?.user_results?.result?.legacy; t.legacy.id_str = t.rest_id; const quote = t.quoted_status_result?.result; if (quote) { t.legacy.quoted_status = quote.legacy; t.legacy.quoted_status.user = quote.core.user_result?.result?.legacy || quote.core.user_results?.result?.legacy; } if (t.note_tweet) { const tmp = t.note_tweet.note_tweet_results.result; t.legacy.entities.hashtags = tmp.entity_set.hashtags; t.legacy.entities.symbols = tmp.entity_set.symbols; t.legacy.entities.urls = tmp.entity_set.urls; t.legacy.entities.user_mentions = tmp.entity_set.user_mentions; t.legacy.full_text = tmp.text; } } const legacy = tweet.legacy; if (legacy) { if (retweet) legacy.retweeted_status = retweet.legacy; if (userId === void 0 || legacy.user_id_str === userId + "") tweets.push(legacy); } } } return tweets; } const getUserTweetsByID = async (id, params = {}) => gatherLegacyFromData$1(await timelineTweets(id, params)); const getUserTweetsAndRepliesByID = async (id, params = {}) => gatherLegacyFromData$1(await timelineTweetsAndReplies(id, params), ["profile-conversation-"], id); const getUserMediaByID = async (id, params = {}) => gatherLegacyFromData$1(await timelineMedia(id, params)); const getUserTweetByStatus = async (id, params = {}) => gatherLegacyFromData$1(await tweetDetail(id, params), ["homeConversation-", "conversationthread-"]); const getListById = async (id, params = {}) => gatherLegacyFromData$1(await listTweets(id, params)); const excludeRetweet = function(tweets) { const excluded = []; for (const t of tweets) { if (t.retweeted_status) continue; excluded.push(t); } return excluded; }; const userByScreenName = (screenName) => twitterGot$1(`${baseUrl$1}${gqlMap$1.UserResultByScreenName}`, { variables: `{"screen_name":"${screenName}","withHighlightedLabel":true}`, features: gqlFeatures$1 }); const userByRestId = (restId) => twitterGot$1(`${baseUrl$1}${gqlMap$1.UserByRestId}`, { variables: `{"userId":"${restId}","withHighlightedLabel":true}`, features: gqlFeatures$1 }); const userByAuto = (id) => { if (id.startsWith("+")) return userByRestId(id.slice(1)); return userByScreenName(id); }; const getUserData$1 = (id) => cache_default.tryGet(`twitter-userdata-${id}`, () => userByAuto(id)); const getUserID = async (id) => { const userData = await getUserData$1(id); return (userData.data?.user || userData.data?.user_result)?.result?.rest_id; }; const getUser$1 = async (id) => { const userData = await getUserData$1(id); return (userData.data?.user || userData.data?.user_result)?.result?.legacy; }; const cacheTryGet$1 = async (_id, params, func) => { const id = await getUserID(_id); if (id === void 0) throw new invalid_parameter_default("User not found"); const funcName = func.name; const paramsString = JSON.stringify(params); return cache_default.tryGet(`twitter:${id}:${funcName}:${paramsString}`, () => func(id, params), config.cache.routeExpire, false); }; const _getUserTweets = (id, params = {}) => cacheTryGet$1(id, params, getUserTweetsByID); const getUserTweets$1 = async (id, params = {}) => { let tweets = []; const rest_id = await getUserID(id); await Promise.all([ _getUserTweets, getUserTweetsAndReplies$1, getUserMedia$1 ].map(async (func) => { try { tweets.push(...await func(id, params)); } catch (error) { logger_default.warn(`Failed to get tweets for ${id} with ${func.name}: ${error}`); } })); const cacheKey = `twitter:user:tweets-cache:${rest_id}`; let cacheValue = await cache_default.get(cacheKey); if (cacheValue) { cacheValue = JSON.parse(cacheValue); if (cacheValue && cacheValue.length) tweets = [...cacheValue, ...tweets]; } const idSet = /* @__PURE__ */ new Set(); tweets = tweets.filter((tweet) => !tweet.in_reply_to_user_id_str || tweet.in_reply_to_user_id_str === rest_id).map((tweet) => { const id_str = tweet.id_str || tweet.conversation_id_str; return !idSet.has(id_str) && idSet.add(id_str) && tweet; }).filter(Boolean).toSorted((a, b) => (b.id_str || b.conversation_id_str) - (a.id_str || a.conversation_id_str)).slice(0, 20); cache_default.set(cacheKey, JSON.stringify(tweets)); return tweets; }; const getUserTweetsAndReplies$1 = (id, params = {}) => cacheTryGet$1(id, params, getUserTweetsAndRepliesByID); const getUserMedia$1 = (id, params = {}) => cacheTryGet$1(id, params, getUserMediaByID); const getUserTweet$1 = (id, params) => cacheTryGet$1(id, params, getUserTweetByStatus); const getSearch$1 = async (keywords, params = {}) => gatherLegacyFromData$1(await timelineKeywords(keywords, params)); const getList$1 = (id, params = {}) => cache_default.tryGet(`twitter:${id}:getListById:${JSON.stringify(params)}`, () => getListById(id, params), config.cache.routeExpire, false); var api_default = { getUser: getUser$1, getUserTweets: getUserTweets$1, getUserTweetsAndReplies: getUserTweetsAndReplies$1, getUserMedia: getUserMedia$1, excludeRetweet, getSearch: getSearch$1, getList: getList$1, getUserTweet: getUserTweet$1, init: () => void 0 }; //#endregion //#region lib/routes/twitter/api/web-api/constants.ts const baseUrl = "https://x.com/i/api"; const gqlMap = Object.fromEntries([ "/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets", "/graphql/Yka-W8dz7RaEuQNkroPkYw/UserByScreenName", "/graphql/HJFjzBgCs16TqxewQOeLNg/HomeTimeline", "/graphql/DiTkXJgLqBBxCs7zaYsbtA/HomeLatestTimeline", "/graphql/bt4TKuFz4T7Ckk-VvQVSow/UserTweetsAndReplies", "/graphql/dexO_2tohK86JDudXXG3Yw/UserMedia", "/graphql/Qw77dDjp9xCpUY-AXwt-yQ/UserByRestId", "/graphql/UN1i3zUiCWa-6r-Uaho4fw/SearchTimeline", "/graphql/Pa45JvqZuKcW1plybfgBlQ/ListLatestTweetsTimeline", "/graphql/QuBlQ6SxNAQCt6-kBiCXCQ/TweetDetail" ].map((endpoint) => [endpoint.split("/")[3].replace(/V2$|Query$|QueryV2$/, ""), endpoint])); const thirdPartySupportedAPI = [ "UserByScreenName", "UserByRestId", "UserTweets", "UserTweetsAndReplies", "ListLatestTweetsTimeline", "SearchTimeline" ]; const gqlFeatureUser = { hidden_profile_subscriptions_enabled: true, rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, subscriptions_verification_info_is_identity_verified_enabled: true, subscriptions_verification_info_verified_since_enabled: true, highlights_tweets_tab_ui_enabled: true, responsive_web_twitter_article_notes_tab_enabled: true, subscriptions_feature_can_gift_premium: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true }; const gqlFeatureFeed = { rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_enhance_cards_enabled: false }; const TweetDetailFeatures = { rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_enhance_cards_enabled: false }; const gqlFeatures = { UserByScreenName: gqlFeatureUser, UserByRestId: gqlFeatureUser, UserTweets: gqlFeatureFeed, UserTweetsAndReplies: gqlFeatureFeed, UserMedia: gqlFeatureFeed, SearchTimeline: gqlFeatureFeed, ListLatestTweetsTimeline: gqlFeatureFeed, HomeTimeline: gqlFeatureFeed, HomeLatestTimeline: TweetDetailFeatures, TweetDetail: TweetDetailFeatures, Likes: gqlFeatureFeed }; const bearerToken = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; //#endregion //#region lib/routes/twitter/api/web-api/login.ts const loginLimiterQueue = new RateLimiterQueue(cache_default.clients.redisClient ? new RateLimiterRedis({ points: 1, duration: 20, execEvenly: true, storeClient: cache_default.clients.redisClient }) : new RateLimiterMemory({ points: 1, duration: 20, execEvenly: true })); async function login({ username, password, authenticationSecret }) { if (!username || !password) return; try { await loginLimiterQueue.removeTokens(1); const cookieJar = new CookieJar(); const browser = await puppeteer_default(); const page = await browser.newPage(); await page.goto("https://x.com/i/flow/login"); await page.waitForSelector("input[autocomplete=\"username\"]"); await page.type("input[autocomplete=\"username\"]", username); await (await page.$$("button"))[3]?.click(); await page.waitForSelector("input[autocomplete=\"current-password\"]"); await page.type("input[autocomplete=\"current-password\"]", password); (await page.waitForSelector("button[data-testid=\"LoginForm_Login_Button\"]"))?.click(); if (authenticationSecret) { await page.waitForSelector("input[inputmode=\"numeric\"]"); const token = authenticator.generate(authenticationSecret); await page.type("input[inputmode=\"numeric\"]", token); (await page.waitForSelector("button[data-testid=\"ocfEnterTextNextButton\"]"))?.click(); } const cookieString = await new Promise((resolve) => { page.on("response", async (response) => { if (response.url().includes("/HomeTimeline")) { if ((await response.json())?.data?.home?.home_timeline_urt?.instructions?.[0]?.entries?.[0]?.entryId === "messageprompt-suspended-prompt") { logger_default.error(`twitter debug: twitter username ${username} login failed: messageprompt-suspended-prompt`); resolve(""); } const cookies = await page.cookies(); for (const cookie$1 of cookies) cookieJar.setCookieSync(`${cookie$1.name}=${cookie$1.value}`, "https://x.com"); logger_default.debug(`twitter debug: twitter username ${username} login success`); resolve(JSON.stringify(cookieJar.serializeSync())); } }); }); await browser.close(); return cookieString; } catch (error) { logger_default.error(`twitter debug: twitter username ${username} login failed:`, error); } } var login_default = login; //#endregion //#region lib/routes/twitter/api/web-api/utils.ts let authTokenIndex = 0; const token2Cookie = async (token) => { const c = await cache_default.get(`twitter:cookie:${token}`); if (c) return c; const jar = new CookieJar(); await jar.setCookie(`auth_token=${token}`, "https://x.com"); try { const agent = proxy_default.proxyUri ? new ProxyAgent({ factory: (origin, opts) => new Client(origin, opts).compose(cookie({ jar })), uri: proxy_default.proxyUri }) : new CookieAgent({ cookies: { jar } }); if (token) await ofetch_default("https://x.com", { dispatcher: agent }); else { const gt = (await ofetch_default("https://x.com/narendramodi?mx=2", { dispatcher: agent })).match(/document\.cookie="gt=(\d+)/)?.[1]; if (gt) jar.setCookieSync(`gt=${gt}`, "https://x.com"); } const cookie$1 = JSON.stringify(jar.serializeSync()); cache_default.set(`twitter:cookie:${token}`, cookie$1); return cookie$1; } catch { return ""; } }; const lockPrefix = "twitter:lock-token1:"; const getAuth = async (retry) => { if (config.twitter.authToken && retry > 0) { const index = authTokenIndex++ % config.twitter.authToken.length; const token = config.twitter.authToken[index]; if (await cache_default.get(`${lockPrefix}${token}`, false)) { logger_default.debug(`twitter debug: twitter cookie for token ${token} is locked, retry: ${retry}`); await new Promise((resolve) => setTimeout(resolve, Math.random() * 500 + 500)); return await getAuth(retry - 1); } else { logger_default.debug(`twitter debug: lock twitter cookie for token ${token}`); await cache_default.set(`${lockPrefix}${token}`, "1", 20); return { token, username: config.twitter.username?.[index], password: config.twitter.password?.[index], authenticationSecret: config.twitter.authenticationSecret?.[index] }; } } }; const twitterGot = async (url, params, options) => { const auth = await getAuth(30); if (!auth && !options?.allowNoAuth) throw new config_not_found_default("No valid Twitter token found"); const requestUrl = `${url}?${queryString.stringify(params)}`; let cookie$1 = await token2Cookie(auth?.token); if (!cookie$1 && auth) cookie$1 = await login_default({ username: auth.username, password: auth.password, authenticationSecret: auth.authenticationSecret }); let dispatchers; if (cookie$1) { logger_default.debug(`twitter debug: got twitter cookie for token ${auth?.token}`); if (typeof cookie$1 === "string") cookie$1 = JSON.parse(cookie$1); const jar = CookieJar.deserializeSync(cookie$1); const agent = proxy_default.proxyUri ? new ProxyAgent({ factory: (origin, opts) => new Client(origin, opts).compose(cookie({ jar })), uri: proxy_default.proxyUri }) : new CookieAgent({ cookies: { jar } }); if (proxy_default.proxyUri) logger_default.debug(`twitter debug: Proxying request: ${requestUrl}`); dispatchers = { jar, agent }; } else if (auth) throw new config_not_found_default(`Twitter cookie for token ${auth?.token?.replace(/(\w{8})(\w+)/, (_, v1, v2) => v1 + "*".repeat(v2.length))} is not valid`); const jsonCookie = dispatchers ? Object.fromEntries(dispatchers.jar.getCookieStringSync(url).split(";").map((c) => Cookie.parse(c)?.toJSON()).map((c) => [c?.key, c?.value])) : {}; const response = await ofetch_default.raw(requestUrl, { retry: 0, headers: { authority: "x.com", accept: "*/*", "accept-language": "en-US,en;q=0.9", authorization: bearerToken, "cache-control": "no-cache", "content-type": "application/json", dnt: "1", pragma: "no-cache", referer: "https://x.com/", "x-twitter-active-user": "yes", "x-twitter-client-language": "en", "x-csrf-token": jsonCookie.ct0, ...auth?.token ? { "x-twitter-auth-type": "OAuth2Session" } : { "x-guest-token": jsonCookie.gt } }, dispatcher: dispatchers?.agent, onResponse: async ({ response: response$1 }) => { const remaining = response$1.headers.get("x-rate-limit-remaining"); const remainingInt = Number.parseInt(remaining || "0"); const reset = response$1.headers.get("x-rate-limit-reset"); logger_default.debug(`twitter debug: twitter rate limit remaining for token ${auth?.token} is ${remaining} and reset at ${reset}, auth: ${JSON.stringify(auth)}, status: ${response$1.status}, data: ${JSON.stringify(response$1._data?.data)}, cookie: ${JSON.stringify(dispatchers?.jar.serializeSync())}`); if (auth) if (remaining && remainingInt < 2 && reset) { const delay = ((/* @__PURE__ */ new Date(Number.parseInt(reset) * 1e3)).getTime() - Date.now()) / 1e3; logger_default.debug(`twitter debug: twitter rate limit exceeded for token ${auth.token} with status ${response$1.status}, will unlock after ${delay}s`); await cache_default.set(`${lockPrefix}${auth.token}`, "1", Math.ceil(delay) * 2); } else if (response$1.status === 429 || JSON.stringify(response$1._data?.data) === "{\"user\":{}}") { logger_default.debug(`twitter debug: twitter rate limit exceeded for token ${auth.token} with status ${response$1.status}`); await cache_default.set(`${lockPrefix}${auth.token}`, "1", 2e3); } else if (response$1.status === 403 || response$1.status === 401) { const newCookie = await login_default({ username: auth.username, password: auth.password, authenticationSecret: auth.authenticationSecret }); if (newCookie) { logger_default.debug(`twitter debug: reset twitter cookie for token ${auth.token}, ${newCookie}`); await cache_default.set(`twitter:cookie:${auth.token}`, newCookie, config.cache.contentExpire); logger_default.debug(`twitter debug: unlock twitter cookie for token ${auth.token} with error1`); await cache_default.set(`${lockPrefix}${auth.token}`, "", 1); } else { const tokenIndex$1 = config.twitter.authToken?.indexOf(auth.token); if (tokenIndex$1 !== void 0 && tokenIndex$1 !== -1) config.twitter.authToken?.splice(tokenIndex$1, 1); if (auth.username) { const usernameIndex = config.twitter.username?.indexOf(auth.username); if (usernameIndex !== void 0 && usernameIndex !== -1) config.twitter.username?.splice(usernameIndex, 1); } if (auth.password) { const passwordIndex = config.twitter.password?.indexOf(auth.password); if (passwordIndex !== void 0 && passwordIndex !== -1) config.twitter.password?.splice(passwordIndex, 1); } logger_default.debug(`twitter debug: delete twitter cookie for token ${auth.token} with status ${response$1.status}, remaining tokens: ${config.twitter.authToken?.length}`); await cache_default.set(`${lockPrefix}${auth.token}`, "1", 3600); } } else { logger_default.debug(`twitter debug: unlock twitter cookie with success for token ${auth.token}`); await cache_default.set(`${lockPrefix}${auth.token}`, "", 1); } } }); if (auth?.token) { logger_default.debug(`twitter debug: update twitter cookie for token ${auth.token}`); await cache_default.set(`twitter:cookie:${auth.token}`, JSON.stringify(dispatchers?.jar.serializeSync()), config.cache.contentExpire); } return response._data; }; const paginationTweets = async (endpoint, userId, variables, path) => { const params = { variables: JSON.stringify({ ...variables, userId }), features: JSON.stringify(gqlFeatures[endpoint]) }; const fetchData = async () => { if (config.twitter.thirdPartyApi && thirdPartySupportedAPI.includes(endpoint)) { const { data: data$1 } = await ofetch_default(`${config.twitter.thirdPartyApi}${gqlMap[endpoint]}`, { method: "GET", params, headers: { "accept-encoding": "gzip" } }); return data$1; } const { data } = await twitterGot(baseUrl + gqlMap[endpoint], params); return data; }; const getInstructions = (data) => { if (path) { let instructions$2 = data; for (const p of path) instructions$2 = instructions$2[p]; return instructions$2.instructions; } const userResult = data?.user?.result; const instructions$1 = (userResult?.timeline?.timeline || userResult?.timeline?.timeline_v2 || userResult?.timeline_v2?.timeline)?.instructions; if (!instructions$1) logger_default.debug(`twitter debug: instructions not found in data: ${JSON.stringify(data)}`); return instructions$1; }; const instructions = getInstructions(await fetchData()); if (!instructions) return []; const moduleItems = instructions.find((i) => i.type === "TimelineAddToModule")?.moduleItems; const entries = instructions.find((i) => i.type === "TimelineAddEntries")?.entries; return moduleItems || entries || []; }; function gatherLegacyFromData(entries, filterNested, userId) { const tweets = []; const filteredEntries = []; for (const entry of entries) { const entryId = entry.entryId; if (entryId) { if (entryId.startsWith("tweet-")) filteredEntries.push(entry); else if (entryId.startsWith("profile-grid-0-tweet-")) filteredEntries.push(entry); if (filterNested && filterNested.some((f) => entryId.startsWith(f))) filteredEntries.push(...entry.content.items); } } for (const entry of filteredEntries) if (entry.entryId) { const content = entry.content || entry.item; let tweet = content?.content?.tweetResult?.result || content?.itemContent?.tweet_results?.result; if (tweet && tweet.tweet) tweet = tweet.tweet; if (tweet) { const retweet = tweet.legacy?.retweeted_status_result?.result; for (const t of [tweet, retweet]) { if (!t?.legacy) continue; t.legacy.user = t.core?.user_result?.result?.legacy || t.core?.user_results?.result?.legacy; if (t.legacy.user && t.core?.user_results?.result?.core) { const coreUser = t.core.user_results.result.core; if (coreUser.name) t.legacy.user.name = coreUser.name; if (coreUser.screen_name) t.legacy.user.screen_name = coreUser.screen_name; } t.legacy.id_str = t.rest_id; const quote = t.quoted_status_result?.result?.tweet || t.quoted_status_result?.result; if (quote) { t.legacy.quoted_status = quote.legacy; t.legacy.quoted_status.user = quote.core.user_result?.result?.legacy || quote.core.user_results?.result?.legacy; if (t.legacy.quoted_status.user && quote.core?.user_results?.result?.core) { const quoteCoreUser = quote.core.user_results.result.core; if (quoteCoreUser.name) t.legacy.quoted_status.user.name = quoteCoreUser.name; if (quoteCoreUser.screen_name) t.legacy.quoted_status.user.screen_name = quoteCoreUser.screen_name; } } if (t.note_tweet) { const tmp = t.note_tweet.note_tweet_results.result; t.legacy.entities.hashtags = tmp.entity_set.hashtags; t.legacy.entities.symbols = tmp.entity_set.symbols; t.legacy.entities.urls = tmp.entity_set.urls; t.legacy.entities.user_mentions = tmp.entity_set.user_mentions; t.legacy.full_text = tmp.text; } } const legacy = tweet.legacy; if (legacy) { if (retweet) legacy.retweeted_status = retweet.legacy; if (userId === void 0 || legacy.user_id_str === userId + "") tweets.push(legacy); } } } return tweets; } //#endregion //#region lib/routes/twitter/api/web-api/api.ts const getUserData = (id) => cache_default.tryGet(`twitter-userdata-${id}`, () => { const params = { variables: id.startsWith("+") ? JSON.stringify({ userId: id.slice(1), withSafetyModeUserFields: true }) : JSON.stringify({ screen_name: id, withSafetyModeUserFields: true }), features: JSON.stringify(id.startsWith("+") ? gqlFeatures.UserByRestId : gqlFeatures.UserByScreenName), fieldToggles: JSON.stringify({ withAuxiliaryUserLabels: false }) }; if (config.twitter.thirdPartyApi) { const endpoint = id.startsWith("+") ? gqlMap.UserByRestId : gqlMap.UserByScreenName; return ofetch_default(`${config.twitter.thirdPartyApi}${endpoint}`, { method: "GET", params, headers: { "accept-encoding": "gzip" } }); } return twitterGot(`${baseUrl}${id.startsWith("+") ? gqlMap.UserByRestId : gqlMap.UserByScreenName}`, params, { allowNoAuth: !id.startsWith("+") }); }); const cacheTryGet = async (_id, params, func) => { const userData = await getUserData(_id); const id = (userData.data?.user || userData.data?.user_result)?.result?.rest_id; if (id === void 0) { cache_default.set(`twitter-userdata-${_id}`, "", config.cache.contentExpire); throw new invalid_parameter_default("User not found"); } const funcName = func.name; const paramsString = JSON.stringify(params); return cache_default.tryGet(`twitter:${id}:${funcName}:${paramsString}`, () => func(id, params), config.cache.routeExpire, false); }; const getUserTweets = (id, params) => cacheTryGet(id, params, async (id$1, params$1 = {}) => gatherLegacyFromData(await paginationTweets("UserTweets", id$1, { ...params$1, count: 20, includePromotedContent: true, withQuickPromoteEligibilityTweetFields: true, withVoice: true, withV2Timeline: true }))); const getUserTweetsAndReplies = (id, params) => cacheTryGet(id, params, async (id$1, params$1 = {}) => gatherLegacyFromData(await paginationTweets("UserTweetsAndReplies", id$1, { ...params$1, count: 20, includePromotedContent: true, withCommunity: true, withVoice: true, withV2Timeline: true }), ["profile-conversation-"], id$1)); const getUserMedia = (id, params) => cacheTryGet(id, params, async (id$1, params$1 = {}) => { const cursor = (await paginationTweets("UserMedia", id$1, { ...params$1, count: 20, includePromotedContent: false, withClientEventToken: false, withBirdwatchNotes: false, withVoice: true, withV2Timeline: true })).find((i) => i.content?.cursorType === "Top").content.value; return gatherLegacyFromData(await paginationTweets("UserMedia", id$1, { ...params$1, cursor, count: 20, includePromotedContent: false, withClientEventToken: false, withBirdwatchNotes: false, withVoice: true, withV2Timeline: true })); }); const getUserLikes = (id, params) => cacheTryGet(id, params, async (id$1, params$1 = {}) => gatherLegacyFromData(await paginationTweets("Likes", id$1, { ...params$1, includeHasBirdwatchNotes: false, includePromotedContent: false, withBirdwatchNotes: false, withVoice: false, withV2Timeline: true }))); const getUserTweet = (id, params) => cacheTryGet(id, params, async (id$1, params$1 = {}) => gatherLegacyFromData(await paginationTweets("TweetDetail", id$1, { ...params$1, includeHasBirdwatchNotes: false, includePromotedContent: false, withBirdwatchNotes: false, withVoice: false, withV2Timeline: true }, ["threaded_conversation_with_injections_v2"]), ["homeConversation-", "conversationthread-"])); const getSearch = async (keywords, params) => gatherLegacyFromData(await paginationTweets("SearchTimeline", void 0, { ...params, rawQuery: keywords, count: 20, querySource: "typed_query", product: "Latest" }, [ "search_by_raw_query", "search_timeline", "timeline" ])); const getList = async (id, params) => gatherLegacyFromData(await paginationTweets("ListLatestTweetsTimeline", void 0, { ...params, listId: id, count: 20 }, [ "list", "tweets_timeline", "timeline" ])); const getUser = async (id) => { const userData = await getUserData(id); return { profile_image_url: userData.data?.user?.result?.avatar?.image_url, ...userData.data?.user?.result?.core, ...(userData.data?.user || userData.data?.user_result)?.result?.legacy }; }; const getHomeTimeline = async (id, params) => gatherLegacyFromData(await paginationTweets("HomeTimeline", void 0, { ...params, count: 20, includePromotedContent: true, latestControlAvailable: true, requestContext: "launch", withCommunity: true }, ["home", "home_timeline_urt"])); const getHomeLatestTimeline = async (id, params) => gatherLegacyFromData(await paginationTweets("HomeLatestTimeline", void 0, { ...params, count: 20, includePromotedContent: true, latestControlAvailable: true, requestContext: "launch", withCommunity: true }, ["home", "home_timeline_urt"])); var api_default$1 = { getUser, getUserTweets, getUserTweetsAndReplies, getUserMedia, getUserLikes, getUserTweet, getSearch, getList, getHomeTimeline, getHomeLatestTimeline, init: () => {} }; //#endregion //#region lib/routes/twitter/api/index.ts const enableThirdPartyApi = config.twitter.thirdPartyApi; const enableMobileApi = config.twitter.username && config.twitter.password; const enableWebApi = config.twitter.authToken; let api = { init: () => { throw new config_not_found_default("Twitter API is not configured"); }, getUser: () => null, getUserTweets: () => null, getUserTweetsAndReplies: () => null, getUserMedia: () => null, getUserLikes: () => null, getUserTweet: () => null, getSearch: () => null, getList: () => null, getHomeTimeline: () => null, getHomeLatestTimeline: () => null }; if (enableThirdPartyApi) api = api_default$1; else if (enableWebApi) api = api_default$1; else if (enableMobileApi) api = api_default; var api_default$2 = api; //#endregion export { api_default$2 as t };