rsshub
Version:
Make RSS Great Again!
1,021 lines (1,011 loc) • 42.7 kB
JavaScript
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 };