Merge PR #48: feat: add Xiaohongshu source + Reddit public fallback
- Xiaohongshu search via local MCP service (opt-in, zero impact if service not running) - Reddit public JSON fallback (works with zero API keys) - Reddit priority: ScrapeCreators -> OpenAI -> public fallback - Updated env.py: Reddit always available via public fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+7
@@ -0,0 +1,7 @@
|
||||
export type CliInvocation = {
|
||||
argv: string[] | null;
|
||||
showHelp: boolean;
|
||||
};
|
||||
export declare function looksLikeTweetInput(value: string): boolean;
|
||||
export declare function resolveCliInvocation(rawArgs: string[], knownCommands: Set<string>): CliInvocation;
|
||||
//# sourceMappingURL=cli-args.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../../src/lib/cli-args.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAKF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAiBjG"}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
const TWEET_URL_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/[^/]+\/status\/\d+/i;
|
||||
const TWEET_ID_REGEX = /^\d{8,}$/;
|
||||
export function looksLikeTweetInput(value) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return TWEET_URL_REGEX.test(trimmed) || TWEET_ID_REGEX.test(trimmed);
|
||||
}
|
||||
export function resolveCliInvocation(rawArgs, knownCommands) {
|
||||
if (rawArgs.length === 0) {
|
||||
return { argv: null, showHelp: true };
|
||||
}
|
||||
const hasKnownCommand = rawArgs.some((arg) => knownCommands.has(arg));
|
||||
if (!hasKnownCommand) {
|
||||
const tweetArgIndex = rawArgs.findIndex(looksLikeTweetInput);
|
||||
if (tweetArgIndex >= 0) {
|
||||
const rewrittenArgs = [...rawArgs];
|
||||
rewrittenArgs.splice(tweetArgIndex, 0, 'read');
|
||||
return { argv: ['node', 'bird', ...rewrittenArgs], showHelp: false };
|
||||
}
|
||||
}
|
||||
return { argv: null, showHelp: false };
|
||||
}
|
||||
//# sourceMappingURL=cli-args.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../src/lib/cli-args.ts"],"names":[],"mappings":"AAKA,MAAM,eAAe,GAAG,yEAAyE,CAAC;AAClG,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAiB,EAAE,aAA0B;IAChF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACxC,CAAC;IAED,MAAM,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAEtE,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC7D,IAAI,aAAa,IAAI,CAAC,EAAE,CAAC;YACvB,MAAM,aAAa,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;YACnC,aAAa,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACvE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACzC,CAAC"}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Browser cookie extraction for Twitter authentication.
|
||||
* Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads.
|
||||
*/
|
||||
export interface TwitterCookies {
|
||||
authToken: string | null;
|
||||
ct0: string | null;
|
||||
cookieHeader: string | null;
|
||||
source: string | null;
|
||||
}
|
||||
export interface CookieExtractionResult {
|
||||
cookies: TwitterCookies;
|
||||
warnings: string[];
|
||||
}
|
||||
export type CookieSource = 'safari' | 'chrome' | 'firefox';
|
||||
export declare function extractCookiesFromSafari(): Promise<CookieExtractionResult>;
|
||||
export declare function extractCookiesFromChrome(profile?: string): Promise<CookieExtractionResult>;
|
||||
export declare function extractCookiesFromFirefox(profile?: string): Promise<CookieExtractionResult>;
|
||||
/**
|
||||
* Resolve Twitter credentials from multiple sources.
|
||||
* Priority: CLI args > environment variables > browsers (ordered).
|
||||
*/
|
||||
export declare function resolveCredentials(options: {
|
||||
authToken?: string;
|
||||
ct0?: string;
|
||||
cookieSource?: CookieSource | CookieSource[];
|
||||
chromeProfile?: string;
|
||||
firefoxProfile?: string;
|
||||
cookieTimeoutMs?: number;
|
||||
}): Promise<CookieExtractionResult>;
|
||||
//# sourceMappingURL=cookies.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/lib/cookies.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAsI3D,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAEhF;AAED,wBAAsB,wBAAwB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEhG;AAED,wBAAsB,yBAAyB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEjG;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0DlC"}
|
||||
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Browser cookie extraction for Twitter authentication.
|
||||
* Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads.
|
||||
*/
|
||||
import { getCookies } from '@steipete/sweet-cookie';
|
||||
const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0'];
|
||||
const TWITTER_URL = 'https://x.com/';
|
||||
const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/'];
|
||||
const DEFAULT_COOKIE_TIMEOUT_MS = 30_000;
|
||||
function normalizeValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
function cookieHeader(authToken, ct0) {
|
||||
return `auth_token=${authToken}; ct0=${ct0}`;
|
||||
}
|
||||
function buildEmpty() {
|
||||
return { authToken: null, ct0: null, cookieHeader: null, source: null };
|
||||
}
|
||||
function readEnvCookie(cookies, keys, field) {
|
||||
if (cookies[field]) {
|
||||
return;
|
||||
}
|
||||
for (const key of keys) {
|
||||
const value = normalizeValue(process.env[key]);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
cookies[field] = value;
|
||||
if (!cookies.source) {
|
||||
cookies.source = `env ${key}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function resolveSources(cookieSource) {
|
||||
if (Array.isArray(cookieSource)) {
|
||||
return cookieSource;
|
||||
}
|
||||
if (cookieSource) {
|
||||
return [cookieSource];
|
||||
}
|
||||
return ['safari', 'chrome', 'firefox'];
|
||||
}
|
||||
function labelForSource(source, profile) {
|
||||
if (source === 'safari') {
|
||||
return 'Safari';
|
||||
}
|
||||
if (source === 'chrome') {
|
||||
return profile ? `Chrome profile "${profile}"` : 'Chrome default profile';
|
||||
}
|
||||
return profile ? `Firefox profile "${profile}"` : 'Firefox default profile';
|
||||
}
|
||||
function pickCookieValue(cookies, name) {
|
||||
const matches = cookies.filter((c) => c?.name === name && typeof c.value === 'string');
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const preferred = matches.find((c) => (c.domain ?? '').endsWith('x.com'));
|
||||
if (preferred?.value) {
|
||||
return preferred.value;
|
||||
}
|
||||
const twitter = matches.find((c) => (c.domain ?? '').endsWith('twitter.com'));
|
||||
if (twitter?.value) {
|
||||
return twitter.value;
|
||||
}
|
||||
return matches[0]?.value ?? null;
|
||||
}
|
||||
async function readTwitterCookiesFromBrowser(options) {
|
||||
const warnings = [];
|
||||
const out = buildEmpty();
|
||||
const { cookies, warnings: providerWarnings } = await getCookies({
|
||||
url: TWITTER_URL,
|
||||
origins: TWITTER_ORIGINS,
|
||||
names: [...TWITTER_COOKIE_NAMES],
|
||||
browsers: [options.source],
|
||||
mode: 'merge',
|
||||
chromeProfile: options.chromeProfile,
|
||||
firefoxProfile: options.firefoxProfile,
|
||||
timeoutMs: options.cookieTimeoutMs,
|
||||
});
|
||||
warnings.push(...providerWarnings);
|
||||
const authToken = pickCookieValue(cookies, 'auth_token');
|
||||
const ct0 = pickCookieValue(cookies, 'ct0');
|
||||
if (authToken) {
|
||||
out.authToken = authToken;
|
||||
}
|
||||
if (ct0) {
|
||||
out.ct0 = ct0;
|
||||
}
|
||||
if (out.authToken && out.ct0) {
|
||||
out.cookieHeader = cookieHeader(out.authToken, out.ct0);
|
||||
out.source = labelForSource(options.source, options.source === 'chrome' ? options.chromeProfile : options.firefoxProfile);
|
||||
return { cookies: out, warnings };
|
||||
}
|
||||
if (options.source === 'safari') {
|
||||
warnings.push('No Twitter cookies found in Safari. Make sure you are logged into x.com in Safari.');
|
||||
}
|
||||
else if (options.source === 'chrome') {
|
||||
warnings.push('No Twitter cookies found in Chrome. Make sure you are logged into x.com in Chrome.');
|
||||
}
|
||||
else {
|
||||
warnings.push('No Twitter cookies found in Firefox. Make sure you are logged into x.com in Firefox and the profile exists.');
|
||||
}
|
||||
return { cookies: out, warnings };
|
||||
}
|
||||
export async function extractCookiesFromSafari() {
|
||||
return readTwitterCookiesFromBrowser({ source: 'safari' });
|
||||
}
|
||||
export async function extractCookiesFromChrome(profile) {
|
||||
return readTwitterCookiesFromBrowser({ source: 'chrome', chromeProfile: profile });
|
||||
}
|
||||
export async function extractCookiesFromFirefox(profile) {
|
||||
return readTwitterCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile });
|
||||
}
|
||||
/**
|
||||
* Resolve Twitter credentials from multiple sources.
|
||||
* Priority: CLI args > environment variables > browsers (ordered).
|
||||
*/
|
||||
export async function resolveCredentials(options) {
|
||||
const warnings = [];
|
||||
const cookies = buildEmpty();
|
||||
const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' &&
|
||||
Number.isFinite(options.cookieTimeoutMs) &&
|
||||
options.cookieTimeoutMs > 0
|
||||
? options.cookieTimeoutMs
|
||||
: process.platform === 'darwin'
|
||||
? DEFAULT_COOKIE_TIMEOUT_MS
|
||||
: undefined;
|
||||
if (options.authToken) {
|
||||
cookies.authToken = options.authToken;
|
||||
cookies.source = 'CLI argument';
|
||||
}
|
||||
if (options.ct0) {
|
||||
cookies.ct0 = options.ct0;
|
||||
if (!cookies.source) {
|
||||
cookies.source = 'CLI argument';
|
||||
}
|
||||
}
|
||||
readEnvCookie(cookies, ['AUTH_TOKEN', 'TWITTER_AUTH_TOKEN'], 'authToken');
|
||||
readEnvCookie(cookies, ['CT0', 'TWITTER_CT0'], 'ct0');
|
||||
if (cookies.authToken && cookies.ct0) {
|
||||
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
||||
return { cookies, warnings };
|
||||
}
|
||||
const sourcesToTry = resolveSources(options.cookieSource);
|
||||
for (const source of sourcesToTry) {
|
||||
const res = await readTwitterCookiesFromBrowser({
|
||||
source,
|
||||
chromeProfile: options.chromeProfile,
|
||||
firefoxProfile: options.firefoxProfile,
|
||||
cookieTimeoutMs,
|
||||
});
|
||||
warnings.push(...res.warnings);
|
||||
if (res.cookies.authToken && res.cookies.ct0) {
|
||||
return { cookies: res.cookies, warnings };
|
||||
}
|
||||
}
|
||||
if (!cookies.authToken) {
|
||||
warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox');
|
||||
}
|
||||
if (!cookies.ct0) {
|
||||
warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox');
|
||||
}
|
||||
if (cookies.authToken && cookies.ct0) {
|
||||
cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0);
|
||||
}
|
||||
return { cookies, warnings };
|
||||
}
|
||||
//# sourceMappingURL=cookies.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
export declare function extractBookmarkFolderId(input: string): string | null;
|
||||
//# sourceMappingURL=extract-bookmark-folder-id.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-bookmark-folder-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-bookmark-folder-id.ts"],"names":[],"mappings":"AAMA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapE"}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Extract bookmark folder (collection) ID from an X/Twitter bookmarks URL.
|
||||
*/
|
||||
const BOOKMARK_FOLDER_URL_REGEX = /(?:twitter\.com|x\.com)\/i\/bookmarks\/(\d+)/i;
|
||||
const BOOKMARK_FOLDER_ID_REGEX = /^\d{5,}$/;
|
||||
export function extractBookmarkFolderId(input) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const urlMatch = BOOKMARK_FOLDER_URL_REGEX.exec(trimmed);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
if (BOOKMARK_FOLDER_ID_REGEX.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=extract-bookmark-folder-id.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-bookmark-folder-id.js","sourceRoot":"","sources":["../../src/lib/extract-bookmark-folder-id.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,yBAAyB,GAAG,+CAA+C,CAAC;AAClF,MAAM,wBAAwB,GAAG,UAAU,CAAC;AAE5C,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare function extractListId(input: string): string | null;
|
||||
//# sourceMappingURL=extract-list-id.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-list-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-list-id.ts"],"names":[],"mappings":"AAMA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa1D"}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// ABOUTME: Extract list ID from an X/Twitter list URL.
|
||||
// ABOUTME: Returns list ID or null if input is invalid.
|
||||
const LIST_URL_REGEX = /(?:twitter\.com|x\.com)\/i\/lists\/(\d+)/i;
|
||||
const LIST_ID_REGEX = /^\d{5,}$/;
|
||||
export function extractListId(input) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const urlMatch = LIST_URL_REGEX.exec(trimmed);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
if (LIST_ID_REGEX.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=extract-list-id.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-list-id.js","sourceRoot":"","sources":["../../src/lib/extract-list-id.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wDAAwD;AAExD,MAAM,cAAc,GAAG,2CAA2C,CAAC;AACnE,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare function extractTweetId(input: string): string;
|
||||
//# sourceMappingURL=extract-tweet-id.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-tweet-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-tweet-id.ts"],"names":[],"mappings":"AAKA,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD"}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Extract tweet ID from a Twitter/X URL or return the input unchanged if it's already an ID.
|
||||
*/
|
||||
const TWEET_URL_REGEX = /(?:twitter\.com|x\.com)\/(?:\w+\/status|i\/web\/status)\/(\d+)/i;
|
||||
export function extractTweetId(input) {
|
||||
// If it's a URL, extract the tweet ID
|
||||
const urlMatch = TWEET_URL_REGEX.exec(input);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
// Assume it's already an ID
|
||||
return input;
|
||||
}
|
||||
//# sourceMappingURL=extract-tweet-id.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"extract-tweet-id.js","sourceRoot":"","sources":["../../src/lib/extract-tweet-id.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,eAAe,GAAG,iEAAiE,CAAC;AAE1F,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,sCAAsC;IACtC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,4BAA4B;IAC5B,OAAO,KAAK,CAAC;AACf,CAAC"}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"global": {
|
||||
"responsive_web_grok_annotations_enabled": false,
|
||||
"post_ctas_fetch_enabled": true,
|
||||
"responsive_web_graphql_exclude_directive_enabled": true
|
||||
},
|
||||
"sets": {
|
||||
"lists": {
|
||||
"blue_business_profile_image_shape_enabled": true,
|
||||
"tweetypie_unmention_optimization_enabled": true,
|
||||
"responsive_web_text_conversations_enabled": false,
|
||||
"interactive_text_enabled": true,
|
||||
"vibe_api_enabled": true,
|
||||
"responsive_web_twitter_blue_verified_badge_is_enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export { type CookieExtractionResult, type CookieSource, extractCookiesFromChrome, extractCookiesFromFirefox, extractCookiesFromSafari, resolveCredentials, type TwitterCookies, } from './cookies.js';
|
||||
export { runtimeQueryIds } from './runtime-query-ids.js';
|
||||
export { type CurrentUserResult, type FollowingResult, type GetTweetResult, type SearchResult, type TweetData, TwitterClient, type TwitterClientOptions, type TwitterUser, } from './twitter-client.js';
|
||||
export type { HomeTimelineFetchOptions } from './twitter-client-home.js';
|
||||
export type { ExploreTab, NewsFetchOptions, NewsItem, NewsResult } from './twitter-client-news.js';
|
||||
export type { SearchFetchOptions } from './twitter-client-search.js';
|
||||
export type { TimelineFetchOptions } from './twitter-client-timelines.js';
|
||||
export type { TweetFetchOptions } from './twitter-client-tweet-detail.js';
|
||||
export type { AboutAccountProfile, AboutAccountResult, TweetResult, UploadMediaResult, } from './twitter-client-types.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,WAAW,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACnG,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,YAAY,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,iBAAiB,GAClB,MAAM,2BAA2B,CAAC"}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { extractCookiesFromChrome, extractCookiesFromFirefox, extractCookiesFromSafari, resolveCredentials, } from './cookies.js';
|
||||
export { runtimeQueryIds } from './runtime-query-ids.js';
|
||||
export { TwitterClient, } from './twitter-client.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAML,aAAa,GAGd,MAAM,qBAAqB,CAAC"}
|
||||
@@ -0,0 +1,6 @@
|
||||
export declare function normalizeHandle(input?: string | null): string | null;
|
||||
export declare function mentionsQueryFromUserOption(userOption?: string | null): {
|
||||
query: string | null;
|
||||
error: string | null;
|
||||
};
|
||||
//# sourceMappingURL=normalize-handle.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"normalize-handle.d.ts","sourceRoot":"","sources":["../../src/lib/normalize-handle.ts"],"names":[],"mappings":"AAEA,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAkBpE;AAED,wBAAgB,2BAA2B,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAcA"}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
const HANDLE_REGEX = /^[A-Za-z0-9_]{1,15}$/;
|
||||
export function normalizeHandle(input) {
|
||||
const raw = (input ?? '').trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const withoutAt = raw.startsWith('@') ? raw.slice(1) : raw;
|
||||
const handle = withoutAt.trim();
|
||||
if (!handle) {
|
||||
return null;
|
||||
}
|
||||
// X/Twitter handles are traditionally max 15 chars; keep strict to avoid surprising queries.
|
||||
if (!HANDLE_REGEX.test(handle)) {
|
||||
return null;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
export function mentionsQueryFromUserOption(userOption) {
|
||||
if (typeof userOption === 'undefined') {
|
||||
return { query: null, error: null };
|
||||
}
|
||||
const handle = normalizeHandle(userOption);
|
||||
if (!handle) {
|
||||
return {
|
||||
query: null,
|
||||
error: 'Invalid --user handle. Expected something like @steipete (letters, digits, underscore; max 15).',
|
||||
};
|
||||
}
|
||||
return { query: `@${handle}`, error: null };
|
||||
}
|
||||
//# sourceMappingURL=normalize-handle.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"normalize-handle.js","sourceRoot":"","sources":["../../src/lib/normalize-handle.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAE5C,MAAM,UAAU,eAAe,CAAC,KAAqB;IACnD,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3D,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6FAA6F;IAC7F,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,UAA0B;IAIpE,IAAI,OAAO,UAAU,KAAK,WAAW,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,iGAAiG;SACzG,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC9C,CAAC"}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
export type OutputConfig = {
|
||||
plain: boolean;
|
||||
emoji: boolean;
|
||||
color: boolean;
|
||||
hyperlinks: boolean;
|
||||
};
|
||||
export type StatusKind = 'ok' | 'warn' | 'err' | 'info' | 'hint';
|
||||
export type LabelKind = 'url' | 'date' | 'source' | 'engine' | 'credentials' | 'user' | 'userId' | 'email';
|
||||
export declare function resolveOutputConfigFromArgv(argv: string[], env: NodeJS.ProcessEnv, isTty: boolean): OutputConfig;
|
||||
export declare function resolveOutputConfigFromCommander(opts: {
|
||||
plain?: boolean;
|
||||
emoji?: boolean;
|
||||
color?: boolean;
|
||||
}, env: NodeJS.ProcessEnv, isTty: boolean): OutputConfig;
|
||||
export declare function statusPrefix(kind: StatusKind, cfg: OutputConfig): string;
|
||||
export declare function labelPrefix(kind: LabelKind, cfg: OutputConfig): string;
|
||||
export declare function formatStatsLine(stats: {
|
||||
likeCount?: number | null;
|
||||
retweetCount?: number | null;
|
||||
replyCount?: number | null;
|
||||
}, cfg: OutputConfig): string;
|
||||
export declare function formatTweetUrl(tweetId: string): string;
|
||||
/**
|
||||
* Wraps a URL in OSC 8 escape sequences to make it clickable in supported terminals.
|
||||
* Falls back to plain text when not in a TTY or when hyperlinks are disabled.
|
||||
*/
|
||||
export declare function hyperlink(url: string, text?: string, cfg?: OutputConfig): string;
|
||||
export declare function formatTweetUrlLine(tweetId: string, cfg: OutputConfig): string;
|
||||
//# sourceMappingURL=output.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AACjE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAqB3G,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,CAUhH;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3D,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,EAAE,OAAO,GACb,YAAY,CAUd;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAQxE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAQtE;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC9F,GAAG,EAAE,YAAY,GAChB,MAAM,CAYR;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,MAAM,CAUhF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAG7E"}
|
||||
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
const STATUS = {
|
||||
ok: { emoji: '✅', text: 'OK:', plain: '[ok]' },
|
||||
warn: { emoji: '⚠️', text: 'Warning:', plain: '[warn]' },
|
||||
err: { emoji: '❌', text: 'Error:', plain: '[err]' },
|
||||
info: { emoji: 'ℹ️', text: 'Info:', plain: '[info]' },
|
||||
hint: { emoji: 'ℹ️', text: 'Hint:', plain: '[hint]' },
|
||||
};
|
||||
const LABELS = {
|
||||
url: { emoji: '🔗', text: 'URL:', plain: 'url:' },
|
||||
date: { emoji: '📅', text: 'Date:', plain: 'date:' },
|
||||
source: { emoji: '📍', text: 'Source:', plain: 'source:' },
|
||||
engine: { emoji: '⚙️', text: 'Engine:', plain: 'engine:' },
|
||||
credentials: { emoji: '🔑', text: 'Credentials:', plain: 'credentials:' },
|
||||
user: { emoji: '🙋', text: 'User:', plain: 'user:' },
|
||||
userId: { emoji: '🪪', text: 'User ID:', plain: 'user_id:' },
|
||||
email: { emoji: '📧', text: 'Email:', plain: 'email:' },
|
||||
};
|
||||
export function resolveOutputConfigFromArgv(argv, env, isTty) {
|
||||
const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb';
|
||||
const defaultColor = isTty && !hasNoColorEnv;
|
||||
const plain = argv.includes('--plain');
|
||||
const emoji = !plain && !argv.includes('--no-emoji');
|
||||
const color = !plain && !argv.includes('--no-color') && defaultColor;
|
||||
const hyperlinks = !plain && isTty;
|
||||
return { plain, emoji, color, hyperlinks };
|
||||
}
|
||||
export function resolveOutputConfigFromCommander(opts, env, isTty) {
|
||||
const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb';
|
||||
const defaultColor = isTty && !hasNoColorEnv;
|
||||
const plain = Boolean(opts.plain);
|
||||
const emoji = !plain && (opts.emoji ?? true);
|
||||
const color = !plain && (opts.color ?? true) && defaultColor;
|
||||
const hyperlinks = !plain && isTty;
|
||||
return { plain, emoji, color, hyperlinks };
|
||||
}
|
||||
export function statusPrefix(kind, cfg) {
|
||||
if (cfg.plain) {
|
||||
return `${STATUS[kind].plain} `;
|
||||
}
|
||||
if (cfg.emoji) {
|
||||
return `${STATUS[kind].emoji} `;
|
||||
}
|
||||
return `${STATUS[kind].text} `;
|
||||
}
|
||||
export function labelPrefix(kind, cfg) {
|
||||
if (cfg.plain) {
|
||||
return `${LABELS[kind].plain} `;
|
||||
}
|
||||
if (cfg.emoji) {
|
||||
return `${LABELS[kind].emoji} `;
|
||||
}
|
||||
return `${LABELS[kind].text} `;
|
||||
}
|
||||
export function formatStatsLine(stats, cfg) {
|
||||
const likeCount = stats.likeCount ?? 0;
|
||||
const retweetCount = stats.retweetCount ?? 0;
|
||||
const replyCount = stats.replyCount ?? 0;
|
||||
if (cfg.plain) {
|
||||
return `likes: ${likeCount} retweets: ${retweetCount} replies: ${replyCount}`;
|
||||
}
|
||||
if (!cfg.emoji) {
|
||||
return `Likes ${likeCount} Retweets ${retweetCount} Replies ${replyCount}`;
|
||||
}
|
||||
return `❤️ ${likeCount} 🔁 ${retweetCount} 💬 ${replyCount}`;
|
||||
}
|
||||
export function formatTweetUrl(tweetId) {
|
||||
return `https://x.com/i/status/${tweetId}`;
|
||||
}
|
||||
/**
|
||||
* Wraps a URL in OSC 8 escape sequences to make it clickable in supported terminals.
|
||||
* Falls back to plain text when not in a TTY or when hyperlinks are disabled.
|
||||
*/
|
||||
export function hyperlink(url, text, cfg) {
|
||||
const displayText = text ?? url;
|
||||
// Only use hyperlinks when explicitly enabled (requires TTY and not plain mode)
|
||||
if (!cfg?.hyperlinks) {
|
||||
return displayText;
|
||||
}
|
||||
const safeUrl = url.replaceAll('\x1b', '').replaceAll('\x07', '');
|
||||
const safeText = displayText.replaceAll('\x1b', '').replaceAll('\x07', '');
|
||||
// OSC 8 hyperlink: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
|
||||
return `\x1b]8;;${safeUrl}\x07${safeText}\x1b]8;;\x07`;
|
||||
}
|
||||
export function formatTweetUrlLine(tweetId, cfg) {
|
||||
const url = formatTweetUrl(tweetId);
|
||||
return `${labelPrefix('url', cfg)}${hyperlink(url, url, cfg)}`;
|
||||
}
|
||||
//# sourceMappingURL=output.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"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"}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
export type CursorPage<T> = {
|
||||
success: true;
|
||||
items: T[];
|
||||
cursor?: string;
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
export type CursorPaginationResult<T> = {
|
||||
success: true;
|
||||
items: T[];
|
||||
nextCursor?: string;
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
items?: T[];
|
||||
nextCursor?: string;
|
||||
};
|
||||
export declare function paginateCursor<T>(opts: {
|
||||
cursor?: string;
|
||||
maxPages?: number;
|
||||
pageDelayMs?: number;
|
||||
sleep: (ms: number) => Promise<void>;
|
||||
getKey: (item: T) => string;
|
||||
fetchPage: (cursor?: string) => Promise<CursorPage<T>>;
|
||||
}): Promise<CursorPaginationResult<T>>;
|
||||
//# sourceMappingURL=paginate-cursor.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"paginate-cursor.d.ts","sourceRoot":"","sources":["../../src/lib/paginate-cursor.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IACpB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAChC;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,wBAAsB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC5B,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;CACxD,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAyCrC"}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export async function paginateCursor(opts) {
|
||||
const { maxPages, pageDelayMs = 1000 } = opts;
|
||||
const seen = new Set();
|
||||
const items = [];
|
||||
let cursor = opts.cursor;
|
||||
let pagesFetched = 0;
|
||||
while (true) {
|
||||
if (pagesFetched > 0 && pageDelayMs > 0) {
|
||||
await opts.sleep(pageDelayMs);
|
||||
}
|
||||
const page = await opts.fetchPage(cursor);
|
||||
if (!page.success) {
|
||||
if (items.length > 0) {
|
||||
return { success: false, error: page.error, items, nextCursor: cursor };
|
||||
}
|
||||
return page;
|
||||
}
|
||||
pagesFetched += 1;
|
||||
for (const item of page.items) {
|
||||
const key = opts.getKey(item);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
items.push(item);
|
||||
}
|
||||
const pageCursor = page.cursor;
|
||||
if (!pageCursor || pageCursor === cursor) {
|
||||
return { success: true, items, nextCursor: undefined };
|
||||
}
|
||||
if (maxPages !== undefined && pagesFetched >= maxPages) {
|
||||
return { success: true, items, nextCursor: pageCursor };
|
||||
}
|
||||
cursor = pageCursor;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=paginate-cursor.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"paginate-cursor.js","sourceRoot":"","sources":["../../src/lib/paginate-cursor.ts"],"names":[],"mappings":"AAwBA,MAAM,CAAC,KAAK,UAAU,cAAc,CAAI,IAOvC;IACC,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAQ,EAAE,CAAC;IACtB,IAAI,MAAM,GAAuB,IAAI,CAAC,MAAM,CAAC;IAC7C,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,YAAY,GAAG,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;YAC1E,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,YAAY,IAAI,CAAC,CAAC;QAElB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,SAAS;YACX,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/B,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;YACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QACzD,CAAC;QAED,IAAI,QAAQ,KAAK,SAAS,IAAI,YAAY,IAAI,QAAQ,EAAE,CAAC;YACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,GAAG,UAAU,CAAC;IACtB,CAAC;AACH,CAAC"}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"CreateTweet": "nmdAQXJDxw6-0KKF2on7eA",
|
||||
"CreateRetweet": "LFho5rIi4xcKO90p9jwG7A",
|
||||
"CreateFriendship": "8h9JVdV8dlSyqyRDJEPCsA",
|
||||
"DestroyFriendship": "ppXWuagMNXgvzx6WoXBW0Q",
|
||||
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
||||
"DeleteBookmark": "Wlmlj2-xzyS1GN3a6cj-mQ",
|
||||
"TweetDetail": "_NvJCnIjOW__EP5-RF197A",
|
||||
"SearchTimeline": "6AAys3t42mosm_yTI_QENg",
|
||||
"Bookmarks": "RV1g3b8n_SGOHwkqKYSCFw",
|
||||
"BookmarkFolderTimeline": "KJIQpsvxrTfRIlbaRIySHQ",
|
||||
"Following": "mWYeougg_ocJS2Vr1Vt28w",
|
||||
"Followers": "SFYY3WsgwjlXSLlfnEUE4A",
|
||||
"Likes": "ETJflBunfqNa1uE1mBPCaw",
|
||||
"ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ",
|
||||
"ExplorePage": "kheAINB_4pzRDqkzG3K-ng",
|
||||
"GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ",
|
||||
"TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ",
|
||||
"AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export type FeatureOverrides = {
|
||||
global?: Record<string, boolean>;
|
||||
sets?: Record<string, Record<string, boolean>>;
|
||||
};
|
||||
type NormalizedFeatureOverrides = {
|
||||
global: Record<string, boolean>;
|
||||
sets: Record<string, Record<string, boolean>>;
|
||||
};
|
||||
export type FeatureOverridesSnapshot = {
|
||||
cachePath: string;
|
||||
overrides: FeatureOverrides;
|
||||
};
|
||||
export declare function loadFeatureOverrides(): NormalizedFeatureOverrides;
|
||||
export declare function getFeatureOverridesSnapshot(): FeatureOverridesSnapshot;
|
||||
export declare function applyFeatureOverrides(setName: string, base: Record<string, boolean>): Record<string, boolean>;
|
||||
export declare function refreshFeatureOverridesCache(): Promise<FeatureOverridesSnapshot>;
|
||||
export declare function clearFeatureOverridesCache(): void;
|
||||
export {};
|
||||
//# sourceMappingURL=runtime-features.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runtime-features.d.ts","sourceRoot":"","sources":["../../src/lib/runtime-features.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAwGF,wBAAgB,oBAAoB,IAAI,0BAA0B,CAgBjE;AAED,wBAAgB,2BAA2B,IAAI,wBAAwB,CAMtE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmB7G;AAED,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAQtF;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
|
||||
import defaultOverrides from './features.json' with { type: 'json' };
|
||||
const DEFAULT_CACHE_FILENAME = 'features.json';
|
||||
let cachedOverrides = null;
|
||||
function normalizeFeatureMap(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
const result = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
if (typeof entry === 'boolean') {
|
||||
result[key] = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function normalizeOverrides(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return { global: {}, sets: {} };
|
||||
}
|
||||
const record = value;
|
||||
const global = normalizeFeatureMap(record.global);
|
||||
const sets = {};
|
||||
const rawSets = record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets)
|
||||
? record.sets
|
||||
: {};
|
||||
for (const [setName, setValue] of Object.entries(rawSets)) {
|
||||
const normalized = normalizeFeatureMap(setValue);
|
||||
if (Object.keys(normalized).length > 0) {
|
||||
sets[setName] = normalized;
|
||||
}
|
||||
}
|
||||
return { global, sets };
|
||||
}
|
||||
function mergeOverrides(base, next) {
|
||||
const sets = { ...base.sets };
|
||||
for (const [setName, overrides] of Object.entries(next.sets)) {
|
||||
const existing = sets[setName];
|
||||
sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides };
|
||||
}
|
||||
return {
|
||||
global: { ...base.global, ...next.global },
|
||||
sets,
|
||||
};
|
||||
}
|
||||
function toFeatureOverrides(overrides) {
|
||||
const result = {};
|
||||
if (Object.keys(overrides.global).length > 0) {
|
||||
result.global = overrides.global;
|
||||
}
|
||||
const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0);
|
||||
if (setEntries.length > 0) {
|
||||
result.sets = Object.fromEntries(setEntries);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function resolveFeaturesCachePath() {
|
||||
const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH;
|
||||
if (override && override.trim().length > 0) {
|
||||
return path.resolve(override.trim());
|
||||
}
|
||||
return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME);
|
||||
}
|
||||
function readOverridesFromFile(cachePath) {
|
||||
if (!existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(cachePath, 'utf8');
|
||||
return normalizeOverrides(JSON.parse(raw));
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function readOverridesFromEnv() {
|
||||
const raw = process.env.BIRD_FEATURES_JSON;
|
||||
if (!raw || raw.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return normalizeOverrides(JSON.parse(raw));
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function writeOverridesToDisk(cachePath, overrides) {
|
||||
const payload = toFeatureOverrides(overrides);
|
||||
return mkdir(path.dirname(cachePath), { recursive: true }).then(() => writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'));
|
||||
}
|
||||
export function loadFeatureOverrides() {
|
||||
if (cachedOverrides) {
|
||||
return cachedOverrides;
|
||||
}
|
||||
const base = normalizeOverrides(defaultOverrides);
|
||||
const fromFile = readOverridesFromFile(resolveFeaturesCachePath());
|
||||
const fromEnv = readOverridesFromEnv();
|
||||
let merged = base;
|
||||
if (fromFile) {
|
||||
merged = mergeOverrides(merged, fromFile);
|
||||
}
|
||||
if (fromEnv) {
|
||||
merged = mergeOverrides(merged, fromEnv);
|
||||
}
|
||||
cachedOverrides = merged;
|
||||
return merged;
|
||||
}
|
||||
export function getFeatureOverridesSnapshot() {
|
||||
const overrides = toFeatureOverrides(loadFeatureOverrides());
|
||||
return {
|
||||
cachePath: resolveFeaturesCachePath(),
|
||||
overrides,
|
||||
};
|
||||
}
|
||||
export function applyFeatureOverrides(setName, base) {
|
||||
const overrides = loadFeatureOverrides();
|
||||
const globalOverrides = overrides.global;
|
||||
const setOverrides = overrides.sets[setName];
|
||||
if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) {
|
||||
return base;
|
||||
}
|
||||
if (setOverrides) {
|
||||
return {
|
||||
...base,
|
||||
...globalOverrides,
|
||||
...setOverrides,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
...globalOverrides,
|
||||
};
|
||||
}
|
||||
export async function refreshFeatureOverridesCache() {
|
||||
const cachePath = resolveFeaturesCachePath();
|
||||
const base = normalizeOverrides(defaultOverrides);
|
||||
const fromFile = readOverridesFromFile(cachePath);
|
||||
const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} });
|
||||
await writeOverridesToDisk(cachePath, merged);
|
||||
cachedOverrides = null;
|
||||
return { cachePath, overrides: toFeatureOverrides(merged) };
|
||||
}
|
||||
export function clearFeatureOverridesCache() {
|
||||
cachedOverrides = null;
|
||||
}
|
||||
//# sourceMappingURL=runtime-features.js.map
|
||||
File diff suppressed because one or more lines are too long
+33
@@ -0,0 +1,33 @@
|
||||
export type RuntimeQueryIdSnapshot = {
|
||||
fetchedAt: string;
|
||||
ttlMs: number;
|
||||
ids: Record<string, string>;
|
||||
discovery: {
|
||||
pages: string[];
|
||||
bundles: string[];
|
||||
};
|
||||
};
|
||||
export type RuntimeQueryIdSnapshotInfo = {
|
||||
snapshot: RuntimeQueryIdSnapshot;
|
||||
cachePath: string;
|
||||
ageMs: number;
|
||||
isFresh: boolean;
|
||||
};
|
||||
export type RuntimeQueryIdsOptions = {
|
||||
cachePath?: string;
|
||||
ttlMs?: number;
|
||||
fetchImpl?: typeof fetch;
|
||||
};
|
||||
export type RuntimeQueryIdStore = {
|
||||
cachePath: string;
|
||||
ttlMs: number;
|
||||
getSnapshotInfo: () => Promise<RuntimeQueryIdSnapshotInfo | null>;
|
||||
getQueryId: (operationName: string) => Promise<string | null>;
|
||||
refresh: (operationNames: string[], opts?: {
|
||||
force?: boolean;
|
||||
}) => Promise<RuntimeQueryIdSnapshotInfo | null>;
|
||||
clearMemory: () => void;
|
||||
};
|
||||
export declare function createRuntimeQueryIdStore(options?: RuntimeQueryIdsOptions): RuntimeQueryIdStore;
|
||||
export declare const runtimeQueryIds: RuntimeQueryIdStore;
|
||||
//# sourceMappingURL=runtime-query-ids.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runtime-query-ids.d.ts","sourceRoot":"","sources":["../../src/lib/runtime-query-ids.ts"],"names":[],"mappings":"AA+CA,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,SAAS,EAAE;QACT,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAAC;IAClE,UAAU,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9D,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAAC;IAC9G,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AA6JF,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,sBAA2B,GAAG,mBAAmB,CAwGnG;AAED,eAAO,MAAM,eAAe,qBAA8B,CAAC"}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
const DEFAULT_CACHE_FILENAME = 'query-ids-cache.json';
|
||||
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const DISCOVERY_PAGES = [
|
||||
'https://x.com/?lang=en',
|
||||
'https://x.com/explore',
|
||||
'https://x.com/notifications',
|
||||
'https://x.com/settings/profile',
|
||||
];
|
||||
const BUNDLE_URL_REGEX = /https:\/\/abs\.twimg\.com\/responsive-web\/client-web(?:-legacy)?\/[A-Za-z0-9.-]+\.js/g;
|
||||
const QUERY_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
const OPERATION_PATTERNS = [
|
||||
{
|
||||
regex: /e\.exports=\{queryId\s*:\s*["']([^"']+)["']\s*,\s*operationName\s*:\s*["']([^"']+)["']/gs,
|
||||
operationGroup: 2,
|
||||
queryIdGroup: 1,
|
||||
},
|
||||
{
|
||||
regex: /e\.exports=\{operationName\s*:\s*["']([^"']+)["']\s*,\s*queryId\s*:\s*["']([^"']+)["']/gs,
|
||||
operationGroup: 1,
|
||||
queryIdGroup: 2,
|
||||
},
|
||||
{
|
||||
regex: /operationName\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)queryId\s*[:=]\s*["']([^"']+)["']/gs,
|
||||
operationGroup: 1,
|
||||
queryIdGroup: 3,
|
||||
},
|
||||
{
|
||||
regex: /queryId\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)operationName\s*[:=]\s*["']([^"']+)["']/gs,
|
||||
operationGroup: 3,
|
||||
queryIdGroup: 1,
|
||||
},
|
||||
];
|
||||
const HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
|
||||
Accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
async function fetchText(fetchImpl, url) {
|
||||
const response = await fetchImpl(url, { headers: HEADERS });
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(`HTTP ${response.status} for ${url}: ${body.slice(0, 120)}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
function resolveDefaultCachePath() {
|
||||
const override = process.env.BIRD_QUERY_IDS_CACHE;
|
||||
if (override && override.trim().length > 0) {
|
||||
return path.resolve(override.trim());
|
||||
}
|
||||
return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME);
|
||||
}
|
||||
function parseSnapshot(raw) {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const record = raw;
|
||||
const fetchedAt = typeof record.fetchedAt === 'string' ? record.fetchedAt : null;
|
||||
const ttlMs = typeof record.ttlMs === 'number' && Number.isFinite(record.ttlMs) ? record.ttlMs : null;
|
||||
const ids = record.ids && typeof record.ids === 'object' ? record.ids : null;
|
||||
const discovery = record.discovery && typeof record.discovery === 'object' ? record.discovery : null;
|
||||
if (!fetchedAt || !ttlMs || !ids || !discovery) {
|
||||
return null;
|
||||
}
|
||||
const pages = Array.isArray(discovery.pages) ? discovery.pages : null;
|
||||
const bundles = Array.isArray(discovery.bundles) ? discovery.bundles : null;
|
||||
if (!pages || !bundles) {
|
||||
return null;
|
||||
}
|
||||
const normalizedIds = {};
|
||||
for (const [key, value] of Object.entries(ids)) {
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
normalizedIds[key] = value.trim();
|
||||
}
|
||||
}
|
||||
return {
|
||||
fetchedAt,
|
||||
ttlMs,
|
||||
ids: normalizedIds,
|
||||
discovery: {
|
||||
pages: pages.filter((p) => typeof p === 'string'),
|
||||
bundles: bundles.filter((b) => typeof b === 'string'),
|
||||
},
|
||||
};
|
||||
}
|
||||
async function readSnapshotFromDisk(cachePath) {
|
||||
try {
|
||||
const raw = await readFile(cachePath, 'utf8');
|
||||
return parseSnapshot(JSON.parse(raw));
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function writeSnapshotToDisk(cachePath, snapshot) {
|
||||
await mkdir(path.dirname(cachePath), { recursive: true });
|
||||
await writeFile(cachePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
async function discoverBundles(fetchImpl) {
|
||||
const bundles = new Set();
|
||||
for (const page of DISCOVERY_PAGES) {
|
||||
try {
|
||||
const html = await fetchText(fetchImpl, page);
|
||||
for (const match of html.matchAll(BUNDLE_URL_REGEX)) {
|
||||
bundles.add(match[0]);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// ignore discovery page failures; other pages often work
|
||||
}
|
||||
}
|
||||
const discovered = [...bundles];
|
||||
if (discovered.length === 0) {
|
||||
throw new Error('No client bundles discovered; x.com layout may have changed.');
|
||||
}
|
||||
return discovered;
|
||||
}
|
||||
function extractOperations(bundleContents, bundleLabel, targets, discovered) {
|
||||
for (const pattern of OPERATION_PATTERNS) {
|
||||
pattern.regex.lastIndex = 0;
|
||||
while (true) {
|
||||
const match = pattern.regex.exec(bundleContents);
|
||||
if (match === null) {
|
||||
break;
|
||||
}
|
||||
const operationName = match[pattern.operationGroup];
|
||||
const queryId = match[pattern.queryIdGroup];
|
||||
if (!operationName || !queryId) {
|
||||
continue;
|
||||
}
|
||||
if (!targets.has(operationName)) {
|
||||
continue;
|
||||
}
|
||||
if (!QUERY_ID_REGEX.test(queryId)) {
|
||||
continue;
|
||||
}
|
||||
if (discovered.has(operationName)) {
|
||||
continue;
|
||||
}
|
||||
discovered.set(operationName, { queryId, bundle: bundleLabel });
|
||||
if (discovered.size === targets.size) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function fetchAndExtract(fetchImpl, bundleUrls, targets) {
|
||||
const discovered = new Map();
|
||||
const CONCURRENCY = 6;
|
||||
for (let i = 0; i < bundleUrls.length; i += CONCURRENCY) {
|
||||
const chunk = bundleUrls.slice(i, i + CONCURRENCY);
|
||||
await Promise.all(chunk.map(async (url) => {
|
||||
if (discovered.size === targets.size) {
|
||||
return;
|
||||
}
|
||||
const label = url.split('/').at(-1) ?? url;
|
||||
try {
|
||||
const js = await fetchText(fetchImpl, url);
|
||||
extractOperations(js, label, targets, discovered);
|
||||
}
|
||||
catch {
|
||||
// ignore failed bundles
|
||||
}
|
||||
}));
|
||||
if (discovered.size === targets.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return discovered;
|
||||
}
|
||||
export function createRuntimeQueryIdStore(options = {}) {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
||||
const cachePath = options.cachePath ? path.resolve(options.cachePath) : resolveDefaultCachePath();
|
||||
let memorySnapshot = null;
|
||||
let loadOnce = null;
|
||||
let refreshInFlight = null;
|
||||
const loadSnapshot = async () => {
|
||||
if (memorySnapshot) {
|
||||
return memorySnapshot;
|
||||
}
|
||||
if (!loadOnce) {
|
||||
loadOnce = (async () => {
|
||||
const fromDisk = await readSnapshotFromDisk(cachePath);
|
||||
memorySnapshot = fromDisk;
|
||||
return fromDisk;
|
||||
})();
|
||||
}
|
||||
return loadOnce;
|
||||
};
|
||||
const getSnapshotInfo = async () => {
|
||||
const snapshot = await loadSnapshot();
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
const fetchedAtMs = new Date(snapshot.fetchedAt).getTime();
|
||||
const ageMs = Number.isFinite(fetchedAtMs) ? Math.max(0, Date.now() - fetchedAtMs) : Number.POSITIVE_INFINITY;
|
||||
const effectiveTtl = Number.isFinite(snapshot.ttlMs) ? snapshot.ttlMs : ttlMs;
|
||||
const isFresh = ageMs <= effectiveTtl;
|
||||
return { snapshot, cachePath, ageMs, isFresh };
|
||||
};
|
||||
const getQueryId = async (operationName) => {
|
||||
const info = await getSnapshotInfo();
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
return info.snapshot.ids[operationName] ?? null;
|
||||
};
|
||||
const refresh = async (operationNames, opts = {}) => {
|
||||
if (refreshInFlight) {
|
||||
return refreshInFlight;
|
||||
}
|
||||
refreshInFlight = (async () => {
|
||||
const current = await getSnapshotInfo();
|
||||
if (!opts.force && current?.isFresh) {
|
||||
return current;
|
||||
}
|
||||
const targets = new Set(operationNames);
|
||||
const bundleUrls = await discoverBundles(fetchImpl);
|
||||
const discovered = await fetchAndExtract(fetchImpl, bundleUrls, targets);
|
||||
if (discovered.size === 0) {
|
||||
return current ?? null;
|
||||
}
|
||||
const ids = {};
|
||||
for (const name of operationNames) {
|
||||
const entry = discovered.get(name);
|
||||
if (entry?.queryId) {
|
||||
ids[name] = entry.queryId;
|
||||
}
|
||||
}
|
||||
const snapshot = {
|
||||
fetchedAt: new Date().toISOString(),
|
||||
ttlMs,
|
||||
ids,
|
||||
discovery: {
|
||||
pages: [...DISCOVERY_PAGES],
|
||||
bundles: bundleUrls.map((url) => url.split('/').at(-1) ?? url),
|
||||
},
|
||||
};
|
||||
await writeSnapshotToDisk(cachePath, snapshot);
|
||||
memorySnapshot = snapshot;
|
||||
return getSnapshotInfo();
|
||||
})().finally(() => {
|
||||
refreshInFlight = null;
|
||||
});
|
||||
return refreshInFlight;
|
||||
};
|
||||
return {
|
||||
cachePath,
|
||||
ttlMs,
|
||||
getSnapshotInfo,
|
||||
getQueryId,
|
||||
refresh,
|
||||
clearMemory() {
|
||||
memorySnapshot = null;
|
||||
loadOnce = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
export const runtimeQueryIds = createRuntimeQueryIdStore();
|
||||
//# sourceMappingURL=runtime-query-ids.js.map
|
||||
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
import type { TweetData, TweetWithMeta } from './twitter-client-types.js';
|
||||
export declare function filterAuthorChain(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[];
|
||||
export declare function filterAuthorOnly(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[];
|
||||
export declare function filterFullChain(tweets: TweetData[], bookmarkedTweet: TweetData, options?: {
|
||||
includeAncestorBranches?: boolean;
|
||||
}): TweetData[];
|
||||
export declare function addThreadMetadata(tweet: TweetData, allConversationTweets: TweetData[]): TweetWithMeta;
|
||||
//# sourceMappingURL=thread-filters.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"thread-filters.d.ts","sourceRoot":"","sources":["../../src/lib/thread-filters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAS1E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,EAAE,SAAS,GAAG,SAAS,EAAE,CAqC9F;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,EAAE,SAAS,GAAG,SAAS,EAAE,CAG7F;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EAAE,EACnB,eAAe,EAAE,SAAS,EAC1B,OAAO,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,SAAS,EAAE,CA4Db;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,GAAG,aAAa,CA0BrG"}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
const sortByCreatedAt = (tweets) => tweets.slice().sort((a, b) => {
|
||||
const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
|
||||
const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
|
||||
return aTime - bTime;
|
||||
});
|
||||
export function filterAuthorChain(tweets, bookmarkedTweet) {
|
||||
const author = bookmarkedTweet.author.username;
|
||||
const byId = new Map(tweets.map((tweet) => [tweet.id, tweet]));
|
||||
const chainIds = new Set();
|
||||
let current = bookmarkedTweet;
|
||||
while (current && current.author.username === author) {
|
||||
chainIds.add(current.id);
|
||||
const parentId = current.inReplyToStatusId;
|
||||
if (!parentId) {
|
||||
break;
|
||||
}
|
||||
const parent = byId.get(parentId);
|
||||
if (!parent || parent.author.username !== author) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const tweet of tweets) {
|
||||
if (tweet.author.username !== author) {
|
||||
continue;
|
||||
}
|
||||
if (chainIds.has(tweet.id)) {
|
||||
continue;
|
||||
}
|
||||
if (tweet.inReplyToStatusId && chainIds.has(tweet.inReplyToStatusId)) {
|
||||
chainIds.add(tweet.id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id)));
|
||||
}
|
||||
export function filterAuthorOnly(tweets, bookmarkedTweet) {
|
||||
const author = bookmarkedTweet.author.username;
|
||||
return tweets.filter((tweet) => tweet.author.username === author);
|
||||
}
|
||||
export function filterFullChain(tweets, bookmarkedTweet, options = {}) {
|
||||
const byId = new Map(tweets.map((tweet) => [tweet.id, tweet]));
|
||||
const repliesByParent = new Map();
|
||||
for (const tweet of tweets) {
|
||||
if (!tweet.inReplyToStatusId) {
|
||||
continue;
|
||||
}
|
||||
const list = repliesByParent.get(tweet.inReplyToStatusId) ?? [];
|
||||
list.push(tweet);
|
||||
repliesByParent.set(tweet.inReplyToStatusId, list);
|
||||
}
|
||||
const chainIds = new Set();
|
||||
const ancestorIds = [];
|
||||
chainIds.add(bookmarkedTweet.id);
|
||||
let current = bookmarkedTweet;
|
||||
while (current?.inReplyToStatusId) {
|
||||
const parent = byId.get(current.inReplyToStatusId);
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
if (!chainIds.has(parent.id)) {
|
||||
chainIds.add(parent.id);
|
||||
ancestorIds.push(parent.id);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
const addDescendants = (startIds) => {
|
||||
const queue = [...startIds];
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift();
|
||||
if (!currentId) {
|
||||
continue;
|
||||
}
|
||||
if (!chainIds.has(currentId)) {
|
||||
chainIds.add(currentId);
|
||||
}
|
||||
const replies = repliesByParent.get(currentId) ?? [];
|
||||
for (const reply of replies) {
|
||||
if (chainIds.has(reply.id)) {
|
||||
continue;
|
||||
}
|
||||
chainIds.add(reply.id);
|
||||
queue.push(reply.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
addDescendants([bookmarkedTweet.id]);
|
||||
if (options.includeAncestorBranches) {
|
||||
for (const ancestorId of ancestorIds) {
|
||||
addDescendants([ancestorId]);
|
||||
}
|
||||
}
|
||||
return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id)));
|
||||
}
|
||||
export function addThreadMetadata(tweet, allConversationTweets) {
|
||||
const author = tweet.author.username;
|
||||
const hasSelfReplies = allConversationTweets.some((candidate) => candidate.inReplyToStatusId === tweet.id && candidate.author.username === author);
|
||||
const isRoot = !tweet.inReplyToStatusId;
|
||||
let threadPosition;
|
||||
if (isRoot && !hasSelfReplies) {
|
||||
threadPosition = 'standalone';
|
||||
}
|
||||
else if (isRoot && hasSelfReplies) {
|
||||
threadPosition = 'root';
|
||||
}
|
||||
else if (!isRoot && hasSelfReplies) {
|
||||
threadPosition = 'middle';
|
||||
}
|
||||
else {
|
||||
threadPosition = 'end';
|
||||
}
|
||||
return {
|
||||
...tweet,
|
||||
isThread: hasSelfReplies || !isRoot,
|
||||
threadPosition,
|
||||
hasSelfReplies,
|
||||
threadRootId: tweet.conversationId ?? null,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=thread-filters.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"thread-filters.js","sourceRoot":"","sources":["../../src/lib/thread-filters.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { type OperationName } from './twitter-client-constants.js';
|
||||
import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
|
||||
export type Constructor<T = object> = new (...args: any[]) => T;
|
||||
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
|
||||
export type Mixin<TBase extends AbstractConstructor<TwitterClientBase>, TAdded> = abstract new (...args: ConstructorParameters<TBase>) => TwitterClientBase & TAdded;
|
||||
export declare abstract class TwitterClientBase {
|
||||
protected authToken: string;
|
||||
protected ct0: string;
|
||||
protected cookieHeader: string;
|
||||
protected userAgent: string;
|
||||
protected timeoutMs?: number;
|
||||
protected quoteDepth: number;
|
||||
protected clientUuid: string;
|
||||
protected clientDeviceId: string;
|
||||
protected clientUserId?: string;
|
||||
constructor(options: TwitterClientOptions);
|
||||
protected abstract getCurrentUser(): Promise<CurrentUserResult>;
|
||||
protected sleep(ms: number): Promise<void>;
|
||||
protected getQueryId(operationName: OperationName): Promise<string>;
|
||||
protected refreshQueryIds(): Promise<void>;
|
||||
protected withRefreshedQueryIdsOn404<T extends {
|
||||
success: boolean;
|
||||
had404?: boolean;
|
||||
}>(attempt: () => Promise<T>): Promise<{
|
||||
result: T;
|
||||
refreshed: boolean;
|
||||
}>;
|
||||
protected getTweetDetailQueryIds(): Promise<string[]>;
|
||||
protected getSearchTimelineQueryIds(): Promise<string[]>;
|
||||
protected fetchWithTimeout(url: string, init: RequestInit): Promise<Response>;
|
||||
protected getHeaders(): Record<string, string>;
|
||||
protected createTransactionId(): string;
|
||||
protected getBaseHeaders(): Record<string, string>;
|
||||
protected getJsonHeaders(): Record<string, string>;
|
||||
protected getUploadHeaders(): Record<string, string>;
|
||||
protected ensureClientUserId(): Promise<void>;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-base.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-base.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-base.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAyC,MAAM,+BAA+B,CAAC;AAC1G,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAIzF,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEhE,MAAM,MAAM,mBAAmB,CAAC,CAAC,GAAG,MAAM,IAAI,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AACjF,MAAM,MAAM,KAAK,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAAE,MAAM,IAAI,QAAQ,MACxF,GAAG,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC,KAClC,iBAAiB,GAAG,MAAM,CAAC;AAEhC,8BAAsB,iBAAiB;IACrC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,cAAc,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEpB,OAAO,EAAE,oBAAoB;IAgBzC,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,iBAAiB,CAAC;cAE/C,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cAIhC,UAAU,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;cAKzD,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;cAWhC,0BAA0B,CAAC,CAAC,SAAS;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EACzF,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;cAU7B,sBAAsB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;cAK3C,yBAAyB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;cAK9C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAcnF,SAAS,CAAC,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI9C,SAAS,CAAC,mBAAmB,IAAI,MAAM;IAIvC,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IA0BlD,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAOlD,SAAS,CAAC,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;cAKpC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CAYpD"}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { randomBytes, randomUUID } from 'node:crypto';
|
||||
import { runtimeQueryIds } from './runtime-query-ids.js';
|
||||
import { QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twitter-client-constants.js';
|
||||
import { normalizeQuoteDepth } from './twitter-client-utils.js';
|
||||
export class TwitterClientBase {
|
||||
authToken;
|
||||
ct0;
|
||||
cookieHeader;
|
||||
userAgent;
|
||||
timeoutMs;
|
||||
quoteDepth;
|
||||
clientUuid;
|
||||
clientDeviceId;
|
||||
clientUserId;
|
||||
constructor(options) {
|
||||
if (!options.cookies.authToken || !options.cookies.ct0) {
|
||||
throw new Error('Both authToken and ct0 cookies are required');
|
||||
}
|
||||
this.authToken = options.cookies.authToken;
|
||||
this.ct0 = options.cookies.ct0;
|
||||
this.cookieHeader = options.cookies.cookieHeader || `auth_token=${this.authToken}; ct0=${this.ct0}`;
|
||||
this.userAgent =
|
||||
options.userAgent ||
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
this.timeoutMs = options.timeoutMs;
|
||||
this.quoteDepth = normalizeQuoteDepth(options.quoteDepth);
|
||||
this.clientUuid = randomUUID();
|
||||
this.clientDeviceId = randomUUID();
|
||||
}
|
||||
async sleep(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
async getQueryId(operationName) {
|
||||
const cached = await runtimeQueryIds.getQueryId(operationName);
|
||||
return cached ?? QUERY_IDS[operationName];
|
||||
}
|
||||
async refreshQueryIds() {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await runtimeQueryIds.refresh(TARGET_QUERY_ID_OPERATIONS, { force: true });
|
||||
}
|
||||
catch {
|
||||
// ignore refresh failures; callers will fall back to baked-in IDs
|
||||
}
|
||||
}
|
||||
async withRefreshedQueryIdsOn404(attempt) {
|
||||
const firstAttempt = await attempt();
|
||||
if (firstAttempt.success || !firstAttempt.had404) {
|
||||
return { result: firstAttempt, refreshed: false };
|
||||
}
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await attempt();
|
||||
return { result: secondAttempt, refreshed: true };
|
||||
}
|
||||
async getTweetDetailQueryIds() {
|
||||
const primary = await this.getQueryId('TweetDetail');
|
||||
return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g']));
|
||||
}
|
||||
async getSearchTimelineQueryIds() {
|
||||
const primary = await this.getQueryId('SearchTimeline');
|
||||
return Array.from(new Set([primary, 'M1jEez78PEfVfbQLvlWMvQ', '5h0kNbk3ii97rmfY6CdgAA', 'Tp1sewRU1AsZpBWhqCZicQ']));
|
||||
}
|
||||
async fetchWithTimeout(url, init) {
|
||||
if (!this.timeoutMs || this.timeoutMs <= 0) {
|
||||
return fetch(url, init);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { ...init, signal: controller.signal });
|
||||
}
|
||||
finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
getHeaders() {
|
||||
return this.getJsonHeaders();
|
||||
}
|
||||
createTransactionId() {
|
||||
return randomBytes(16).toString('hex');
|
||||
}
|
||||
getBaseHeaders() {
|
||||
const headers = {
|
||||
accept: '*/*',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
|
||||
'x-csrf-token': this.ct0,
|
||||
'x-twitter-auth-type': 'OAuth2Session',
|
||||
'x-twitter-active-user': 'yes',
|
||||
'x-twitter-client-language': 'en',
|
||||
'x-client-uuid': this.clientUuid,
|
||||
'x-twitter-client-deviceid': this.clientDeviceId,
|
||||
'x-client-transaction-id': this.createTransactionId(),
|
||||
cookie: this.cookieHeader,
|
||||
'user-agent': this.userAgent,
|
||||
origin: 'https://x.com',
|
||||
referer: 'https://x.com/',
|
||||
};
|
||||
if (this.clientUserId) {
|
||||
headers['x-twitter-client-user-id'] = this.clientUserId;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
getJsonHeaders() {
|
||||
return {
|
||||
...this.getBaseHeaders(),
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
}
|
||||
getUploadHeaders() {
|
||||
// Note: do not set content-type; URLSearchParams/FormData need to set it (incl boundary) themselves.
|
||||
return this.getBaseHeaders();
|
||||
}
|
||||
async ensureClientUserId() {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return;
|
||||
}
|
||||
if (this.clientUserId) {
|
||||
return;
|
||||
}
|
||||
const result = await this.getCurrentUser();
|
||||
if (result.success && result.user?.id) {
|
||||
this.clientUserId = result.user.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-base.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-base.js","sourceRoot":"","sources":["../../src/lib/twitter-client-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAsB,SAAS,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAE1G,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAUhE,MAAM,OAAgB,iBAAiB;IAC3B,SAAS,CAAS;IAClB,GAAG,CAAS;IACZ,YAAY,CAAS;IACrB,SAAS,CAAS;IAClB,SAAS,CAAU;IACnB,UAAU,CAAS;IACnB,UAAU,CAAS;IACnB,cAAc,CAAS;IACvB,YAAY,CAAU;IAEhC,YAAY,OAA6B;QACvC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;QAC3C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,IAAI,cAAc,IAAI,CAAC,SAAS,SAAS,IAAI,CAAC,GAAG,EAAE,CAAC;QACpG,IAAI,CAAC,SAAS;YACZ,OAAO,CAAC,SAAS;gBACjB,uHAAuH,CAAC;QAC1H,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,mBAAmB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,GAAG,UAAU,EAAE,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,UAAU,EAAE,CAAC;IACrC,CAAC;IAIS,KAAK,CAAC,KAAK,CAAC,EAAU;QAC9B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1D,CAAC;IAES,KAAK,CAAC,UAAU,CAAC,aAA4B;QACrD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAC/D,OAAO,MAAM,IAAI,SAAS,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC;IAES,KAAK,CAAC,eAAe;QAC7B,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YACpC,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7E,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;QACpE,CAAC;IACH,CAAC;IAES,KAAK,CAAC,0BAA0B,CACxC,OAAyB;QAEzB,MAAM,YAAY,GAAG,MAAM,OAAO,EAAE,CAAC;QACrC,IAAI,YAAY,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;YACjD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC;QACD,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAC7B,MAAM,aAAa,GAAG,MAAM,OAAO,EAAE,CAAC;QACtC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACpD,CAAC;IAES,KAAK,CAAC,sBAAsB;QACpC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;IAC5F,CAAC;IAES,KAAK,CAAC,yBAAyB;QACvC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;QACxD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACtH,CAAC;IAES,KAAK,CAAC,gBAAgB,CAAC,GAAW,EAAE,IAAiB;QAC7D,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;YAC3C,OAAO,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACvE,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAClE,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAES,UAAU;QAClB,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IAC/B,CAAC;IAES,mBAAmB;QAC3B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;IAES,cAAc;QACtB,MAAM,OAAO,GAA2B;YACtC,MAAM,EAAE,KAAK;YACb,iBAAiB,EAAE,gBAAgB;YACnC,aAAa,EACX,iHAAiH;YACnH,cAAc,EAAE,IAAI,CAAC,GAAG;YACxB,qBAAqB,EAAE,eAAe;YACtC,uBAAuB,EAAE,KAAK;YAC9B,2BAA2B,EAAE,IAAI;YACjC,eAAe,EAAE,IAAI,CAAC,UAAU;YAChC,2BAA2B,EAAE,IAAI,CAAC,cAAc;YAChD,yBAAyB,EAAE,IAAI,CAAC,mBAAmB,EAAE;YACrD,MAAM,EAAE,IAAI,CAAC,YAAY;YACzB,YAAY,EAAE,IAAI,CAAC,SAAS;YAC5B,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,gBAAgB;SAC1B,CAAC;QAEF,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,CAAC,0BAA0B,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;QAC1D,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAES,cAAc;QACtB,OAAO;YACL,GAAG,IAAI,CAAC,cAAc,EAAE;YACxB,cAAc,EAAE,kBAAkB;SACnC,CAAC;IACJ,CAAC;IAES,gBAAgB;QACxB,qGAAqG;QACrG,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IAC/B,CAAC;IAES,KAAK,CAAC,kBAAkB;QAChC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YACpC,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC3C,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;YACtC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,CAAC;IACH,CAAC;CACF"}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { BookmarkMutationResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientBookmarkMethods {
|
||||
unbookmark(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
}
|
||||
export declare function withBookmarks<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientBookmarkMethods>;
|
||||
//# sourceMappingURL=twitter-client-bookmarks.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-bookmarks.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-bookmarks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC9D;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAChF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,4BAA4B,CAAC,CAqE5C"}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL } from './twitter-client-constants.js';
|
||||
export function withBookmarks(Base) {
|
||||
class TwitterClientBookmarks extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
async unbookmark(tweetId) {
|
||||
// TODO: verify if DeleteBookmark requires client user ID or additional payload fields; add ensureClientUserId() if needed (needs live API test).
|
||||
const variables = { tweet_id: tweetId };
|
||||
let queryId = await this.getQueryId('DeleteBookmark');
|
||||
let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/DeleteBookmark`;
|
||||
const buildBody = () => JSON.stringify({ variables, queryId });
|
||||
const buildHeaders = () => ({ ...this.getHeaders(), referer: `https://x.com/i/status/${tweetId}` });
|
||||
let body = buildBody();
|
||||
const parseResponse = async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: data.errors.map((e) => e.message).join(', ') };
|
||||
}
|
||||
return { success: true };
|
||||
};
|
||||
try {
|
||||
let response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
if (response.status === 404) {
|
||||
await this.refreshQueryIds();
|
||||
queryId = await this.getQueryId('DeleteBookmark');
|
||||
urlWithOperation = `${TWITTER_API_BASE}/${queryId}/DeleteBookmark`;
|
||||
body = buildBody();
|
||||
response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
if (response.status === 404) {
|
||||
const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
return parseResponse(retry);
|
||||
}
|
||||
}
|
||||
return parseResponse(response);
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return TwitterClientBookmarks;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-bookmarks.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-bookmarks.js","sourceRoot":"","sources":["../../src/lib/twitter-client-bookmarks.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,43 @@
|
||||
export declare const TWITTER_API_BASE = "https://x.com/i/api/graphql";
|
||||
export declare const TWITTER_GRAPHQL_POST_URL = "https://x.com/i/api/graphql";
|
||||
export declare const TWITTER_UPLOAD_URL = "https://upload.twitter.com/i/media/upload.json";
|
||||
export declare const TWITTER_MEDIA_METADATA_URL = "https://x.com/i/api/1.1/media/metadata/create.json";
|
||||
export declare const TWITTER_STATUS_UPDATE_URL = "https://x.com/i/api/1.1/statuses/update.json";
|
||||
export declare const SETTINGS_SCREEN_NAME_REGEX: RegExp;
|
||||
export declare const SETTINGS_USER_ID_REGEX: RegExp;
|
||||
export declare const SETTINGS_NAME_REGEX: RegExp;
|
||||
export declare const FALLBACK_QUERY_IDS: {
|
||||
readonly CreateTweet: "TAJw1rBsjAtdNgTdlo2oeg";
|
||||
readonly CreateRetweet: "ojPdsZsimiJrUGLR1sjUtA";
|
||||
readonly DeleteRetweet: "iQtK4dl5hBmXewYZuEOKVw";
|
||||
readonly CreateFriendship: "8h9JVdV8dlSyqyRDJEPCsA";
|
||||
readonly DestroyFriendship: "ppXWuagMNXgvzx6WoXBW0Q";
|
||||
readonly FavoriteTweet: "lI07N6Otwv1PhnEgXILM7A";
|
||||
readonly UnfavoriteTweet: "ZYKSe-w7KEslx3JhSIk5LA";
|
||||
readonly CreateBookmark: "aoDbu3RHznuiSkQ9aNM67Q";
|
||||
readonly DeleteBookmark: "Wlmlj2-xzyS1GN3a6cj-mQ";
|
||||
readonly TweetDetail: "97JF30KziU00483E_8elBA";
|
||||
readonly SearchTimeline: "M1jEez78PEfVfbQLvlWMvQ";
|
||||
readonly UserArticlesTweets: "8zBy9h4L90aDL02RsBcCFg";
|
||||
readonly UserTweets: "Wms1GvIiHXAPBaCr9KblaA";
|
||||
readonly Bookmarks: "RV1g3b8n_SGOHwkqKYSCFw";
|
||||
readonly Following: "BEkNpEt5pNETESoqMsTEGA";
|
||||
readonly Followers: "kuFUYP9eV1FPoEy4N-pi7w";
|
||||
readonly Likes: "JR2gceKucIKcVNB_9JkhsA";
|
||||
readonly BookmarkFolderTimeline: "KJIQpsvxrTfRIlbaRIySHQ";
|
||||
readonly ListOwnerships: "wQcOSjSQ8NtgxIwvYl1lMg";
|
||||
readonly ListMemberships: "BlEXXdARdSeL_0KyKHHvvg";
|
||||
readonly ListLatestTweetsTimeline: "2TemLyqrMpTeAmysdbnVqw";
|
||||
readonly ListByRestId: "wXzyA5vM_aVkBL9G8Vp3kw";
|
||||
readonly HomeTimeline: "edseUwk9sP5Phz__9TIRnA";
|
||||
readonly HomeLatestTimeline: "iOEZpOdfekFsxSlPQCQtPg";
|
||||
readonly ExploreSidebar: "lpSN4M6qpimkF4nRFPE3nQ";
|
||||
readonly ExplorePage: "kheAINB_4pzRDqkzG3K-ng";
|
||||
readonly GenericTimelineById: "uGSr7alSjR9v6QJAIaqSKQ";
|
||||
readonly TrendHistory: "Sj4T-jSB9pr0Mxtsc1UKZQ";
|
||||
readonly AboutAccountQuery: "zs_jFPFT78rBpXv9Z3U2YQ";
|
||||
};
|
||||
export type OperationName = keyof typeof FALLBACK_QUERY_IDS;
|
||||
export declare const QUERY_IDS: Record<OperationName, string>;
|
||||
export declare const TARGET_QUERY_ID_OPERATIONS: Array<OperationName>;
|
||||
//# sourceMappingURL=twitter-client-constants.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-constants.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-constants.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAC9D,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,kBAAkB,mDAAmD,CAAC;AACnF,eAAO,MAAM,0BAA0B,uDAAuD,CAAC;AAC/F,eAAO,MAAM,yBAAyB,iDAAiD,CAAC;AACxF,eAAO,MAAM,0BAA0B,QAA4B,CAAC;AACpE,eAAO,MAAM,sBAAsB,QAA4B,CAAC;AAChE,eAAO,MAAM,mBAAmB,QAAsC,CAAC;AAKvE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BrB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAE5D,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAGnD,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAsC,KAAK,CAAC,aAAa,CAAC,CAAC"}
|
||||
@@ -0,0 +1,50 @@
|
||||
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
|
||||
import queryIds from './query-ids.json' with { type: 'json' };
|
||||
export const TWITTER_API_BASE = 'https://x.com/i/api/graphql';
|
||||
export const TWITTER_GRAPHQL_POST_URL = 'https://x.com/i/api/graphql';
|
||||
export const TWITTER_UPLOAD_URL = 'https://upload.twitter.com/i/media/upload.json';
|
||||
export const TWITTER_MEDIA_METADATA_URL = 'https://x.com/i/api/1.1/media/metadata/create.json';
|
||||
export const TWITTER_STATUS_UPDATE_URL = 'https://x.com/i/api/1.1/statuses/update.json';
|
||||
export const SETTINGS_SCREEN_NAME_REGEX = /"screen_name":"([^"]+)"/;
|
||||
export const SETTINGS_USER_ID_REGEX = /"user_id"\s*:\s*"(\d+)"/;
|
||||
export const SETTINGS_NAME_REGEX = /"name":"([^"\\]*(?:\\.[^"\\]*)*)"/;
|
||||
// Query IDs rotate frequently; the values in query-ids.json are refreshed by
|
||||
// scripts/update-query-ids.ts. The fallback values keep the client usable if
|
||||
// the file is missing or incomplete.
|
||||
export const FALLBACK_QUERY_IDS = {
|
||||
CreateTweet: 'TAJw1rBsjAtdNgTdlo2oeg',
|
||||
CreateRetweet: 'ojPdsZsimiJrUGLR1sjUtA',
|
||||
DeleteRetweet: 'iQtK4dl5hBmXewYZuEOKVw',
|
||||
CreateFriendship: '8h9JVdV8dlSyqyRDJEPCsA',
|
||||
DestroyFriendship: 'ppXWuagMNXgvzx6WoXBW0Q',
|
||||
FavoriteTweet: 'lI07N6Otwv1PhnEgXILM7A',
|
||||
UnfavoriteTweet: 'ZYKSe-w7KEslx3JhSIk5LA',
|
||||
CreateBookmark: 'aoDbu3RHznuiSkQ9aNM67Q',
|
||||
DeleteBookmark: 'Wlmlj2-xzyS1GN3a6cj-mQ',
|
||||
TweetDetail: '97JF30KziU00483E_8elBA',
|
||||
SearchTimeline: 'M1jEez78PEfVfbQLvlWMvQ',
|
||||
UserArticlesTweets: '8zBy9h4L90aDL02RsBcCFg',
|
||||
UserTweets: 'Wms1GvIiHXAPBaCr9KblaA',
|
||||
Bookmarks: 'RV1g3b8n_SGOHwkqKYSCFw',
|
||||
Following: 'BEkNpEt5pNETESoqMsTEGA',
|
||||
Followers: 'kuFUYP9eV1FPoEy4N-pi7w',
|
||||
Likes: 'JR2gceKucIKcVNB_9JkhsA',
|
||||
BookmarkFolderTimeline: 'KJIQpsvxrTfRIlbaRIySHQ',
|
||||
ListOwnerships: 'wQcOSjSQ8NtgxIwvYl1lMg',
|
||||
ListMemberships: 'BlEXXdARdSeL_0KyKHHvvg',
|
||||
ListLatestTweetsTimeline: '2TemLyqrMpTeAmysdbnVqw',
|
||||
ListByRestId: 'wXzyA5vM_aVkBL9G8Vp3kw',
|
||||
HomeTimeline: 'edseUwk9sP5Phz__9TIRnA',
|
||||
HomeLatestTimeline: 'iOEZpOdfekFsxSlPQCQtPg',
|
||||
ExploreSidebar: 'lpSN4M6qpimkF4nRFPE3nQ',
|
||||
ExplorePage: 'kheAINB_4pzRDqkzG3K-ng',
|
||||
GenericTimelineById: 'uGSr7alSjR9v6QJAIaqSKQ',
|
||||
TrendHistory: 'Sj4T-jSB9pr0Mxtsc1UKZQ',
|
||||
AboutAccountQuery: 'zs_jFPFT78rBpXv9Z3U2YQ',
|
||||
};
|
||||
export const QUERY_IDS = {
|
||||
...FALLBACK_QUERY_IDS,
|
||||
...queryIds,
|
||||
};
|
||||
export const TARGET_QUERY_ID_OPERATIONS = Object.keys(FALLBACK_QUERY_IDS);
|
||||
//# sourceMappingURL=twitter-client-constants.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-constants.js","sourceRoot":"","sources":["../../src/lib/twitter-client-constants.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,OAAO,QAAQ,MAAM,kBAAkB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAC9D,MAAM,CAAC,MAAM,wBAAwB,GAAG,6BAA6B,CAAC;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,gDAAgD,CAAC;AACnF,MAAM,CAAC,MAAM,0BAA0B,GAAG,oDAAoD,CAAC;AAC/F,MAAM,CAAC,MAAM,yBAAyB,GAAG,8CAA8C,CAAC;AACxF,MAAM,CAAC,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAChE,MAAM,CAAC,MAAM,mBAAmB,GAAG,mCAAmC,CAAC;AAEvE,6EAA6E;AAC7E,6EAA6E;AAC7E,qCAAqC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,WAAW,EAAE,wBAAwB;IACrC,aAAa,EAAE,wBAAwB;IACvC,aAAa,EAAE,wBAAwB;IACvC,gBAAgB,EAAE,wBAAwB;IAC1C,iBAAiB,EAAE,wBAAwB;IAC3C,aAAa,EAAE,wBAAwB;IACvC,eAAe,EAAE,wBAAwB;IACzC,cAAc,EAAE,wBAAwB;IACxC,cAAc,EAAE,wBAAwB;IACxC,WAAW,EAAE,wBAAwB;IACrC,cAAc,EAAE,wBAAwB;IACxC,kBAAkB,EAAE,wBAAwB;IAC5C,UAAU,EAAE,wBAAwB;IACpC,SAAS,EAAE,wBAAwB;IACnC,SAAS,EAAE,wBAAwB;IACnC,SAAS,EAAE,wBAAwB;IACnC,KAAK,EAAE,wBAAwB;IAC/B,sBAAsB,EAAE,wBAAwB;IAChD,cAAc,EAAE,wBAAwB;IACxC,eAAe,EAAE,wBAAwB;IACzC,wBAAwB,EAAE,wBAAwB;IAClD,YAAY,EAAE,wBAAwB;IACtC,YAAY,EAAE,wBAAwB;IACtC,kBAAkB,EAAE,wBAAwB;IAC5C,cAAc,EAAE,wBAAwB;IACxC,WAAW,EAAE,wBAAwB;IACrC,mBAAmB,EAAE,wBAAwB;IAC7C,YAAY,EAAE,wBAAwB;IACtC,iBAAiB,EAAE,wBAAwB;CACnC,CAAC;AAIX,MAAM,CAAC,MAAM,SAAS,GAAkC;IACtD,GAAG,kBAAkB;IACrB,GAAI,QAAmD;CACxD,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAyB,CAAC"}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { BookmarkMutationResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientEngagementMethods {
|
||||
/** Like a tweet. */
|
||||
like(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
/** Remove a like from a tweet. */
|
||||
unlike(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
/** Retweet a tweet. */
|
||||
retweet(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
/** Remove a retweet. */
|
||||
unretweet(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
/** Bookmark a tweet. */
|
||||
bookmark(tweetId: string): Promise<BookmarkMutationResult>;
|
||||
}
|
||||
export declare function withEngagement<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientEngagementMethods>;
|
||||
//# sourceMappingURL=twitter-client-engagement.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-engagement.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-engagement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,MAAM,WAAW,8BAA8B;IAC7C,oBAAoB;IACpB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACvD,kCAAkC;IAClC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzD,uBAAuB;IACvB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC1D,wBAAwB;IACxB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC5D,wBAAwB;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC5D;AAED,wBAAgB,cAAc,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EACjF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAkG9C"}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL } from './twitter-client-constants.js';
|
||||
export function withEngagement(Base) {
|
||||
class TwitterClientEngagement extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
async performEngagementMutation(operationName, tweetId) {
|
||||
await this.ensureClientUserId();
|
||||
const variables = operationName === 'DeleteRetweet' ? { tweet_id: tweetId, source_tweet_id: tweetId } : { tweet_id: tweetId };
|
||||
let queryId = await this.getQueryId(operationName);
|
||||
let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/${operationName}`;
|
||||
const buildBody = () => JSON.stringify({ variables, queryId });
|
||||
const buildHeaders = () => ({ ...this.getHeaders(), referer: `https://x.com/i/status/${tweetId}` });
|
||||
let body = buildBody();
|
||||
const parseResponse = async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: data.errors.map((e) => e.message).join(', ') };
|
||||
}
|
||||
return { success: true };
|
||||
};
|
||||
try {
|
||||
let response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
if (response.status === 404) {
|
||||
await this.refreshQueryIds();
|
||||
queryId = await this.getQueryId(operationName);
|
||||
urlWithOperation = `${TWITTER_API_BASE}/${queryId}/${operationName}`;
|
||||
body = buildBody();
|
||||
response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
if (response.status === 404) {
|
||||
const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, {
|
||||
method: 'POST',
|
||||
headers: buildHeaders(),
|
||||
body,
|
||||
});
|
||||
return parseResponse(retry);
|
||||
}
|
||||
}
|
||||
return parseResponse(response);
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
/** Like a tweet. */
|
||||
async like(tweetId) {
|
||||
return this.performEngagementMutation('FavoriteTweet', tweetId);
|
||||
}
|
||||
/** Remove a like from a tweet. */
|
||||
async unlike(tweetId) {
|
||||
return this.performEngagementMutation('UnfavoriteTweet', tweetId);
|
||||
}
|
||||
/** Retweet a tweet. */
|
||||
async retweet(tweetId) {
|
||||
return this.performEngagementMutation('CreateRetweet', tweetId);
|
||||
}
|
||||
/** Remove a retweet. */
|
||||
async unretweet(tweetId) {
|
||||
return this.performEngagementMutation('DeleteRetweet', tweetId);
|
||||
}
|
||||
/** Bookmark a tweet. */
|
||||
async bookmark(tweetId) {
|
||||
return this.performEngagementMutation('CreateBookmark', tweetId);
|
||||
}
|
||||
}
|
||||
return TwitterClientEngagement;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-engagement.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-engagement.js","sourceRoot":"","sources":["../../src/lib/twitter-client-engagement.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,14 @@
|
||||
export declare function buildArticleFeatures(): Record<string, boolean>;
|
||||
export declare function buildTweetDetailFeatures(): Record<string, boolean>;
|
||||
export declare function buildArticleFieldToggles(): Record<string, boolean>;
|
||||
export declare function buildSearchFeatures(): Record<string, boolean>;
|
||||
export declare function buildTweetCreateFeatures(): Record<string, boolean>;
|
||||
export declare function buildTimelineFeatures(): Record<string, boolean>;
|
||||
export declare function buildBookmarksFeatures(): Record<string, boolean>;
|
||||
export declare function buildLikesFeatures(): Record<string, boolean>;
|
||||
export declare function buildListsFeatures(): Record<string, boolean>;
|
||||
export declare function buildHomeTimelineFeatures(): Record<string, boolean>;
|
||||
export declare function buildUserTweetsFeatures(): Record<string, boolean>;
|
||||
export declare function buildFollowingFeatures(): Record<string, boolean>;
|
||||
export declare function buildExploreFeatures(): Record<string, boolean>;
|
||||
//# sourceMappingURL=twitter-client-features.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-features.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-features.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwC9D;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAelE;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyC7D;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuClE;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAY/D;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKhE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5D;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA6C5D;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAInE;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuCjE;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuChE;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9D"}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
import { applyFeatureOverrides } from './runtime-features.js';
|
||||
export function buildArticleFeatures() {
|
||||
return applyFeatureOverrides('article', {
|
||||
rweb_video_screen_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: true,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_exclude_directive_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_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,
|
||||
responsive_web_grok_show_grok_translated_post: false,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
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,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildTweetDetailFeatures() {
|
||||
return applyFeatureOverrides('tweetDetail', {
|
||||
...buildArticleFeatures(),
|
||||
responsive_web_graphql_exclude_directive_enabled: true,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
responsive_web_twitter_article_plain_text_enabled: true,
|
||||
responsive_web_twitter_article_seed_tweet_detail_enabled: true,
|
||||
responsive_web_twitter_article_seed_tweet_summary_enabled: true,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_edit_tweet_api_enabled: true,
|
||||
tweet_awards_web_tipping_enabled: false,
|
||||
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||
verified_phone_label_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildArticleFieldToggles() {
|
||||
return {
|
||||
withPayments: false,
|
||||
withAuxiliaryUserLabels: false,
|
||||
withArticleRichContentState: true,
|
||||
withArticlePlainText: true,
|
||||
withGrokAnalyze: false,
|
||||
withDisallowedReplyControls: false,
|
||||
};
|
||||
}
|
||||
export function buildSearchFeatures() {
|
||||
return applyFeatureOverrides('search', {
|
||||
rweb_video_screen_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: true,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_exclude_directive_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_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,
|
||||
responsive_web_grok_show_grok_translated_post: false,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
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_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
articles_preview_enabled: true,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildTweetCreateFeatures() {
|
||||
return applyFeatureOverrides('tweetCreate', {
|
||||
rweb_video_screen_enabled: true,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_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,
|
||||
responsive_web_grok_show_grok_translated_post: false,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
creator_subscriptions_quote_tweet_preview_enabled: false,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: false,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
articles_preview_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_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,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildTimelineFeatures() {
|
||||
return applyFeatureOverrides('timeline', {
|
||||
...buildSearchFeatures(),
|
||||
blue_business_profile_image_shape_enabled: true,
|
||||
responsive_web_text_conversations_enabled: false,
|
||||
tweetypie_unmention_optimization_enabled: true,
|
||||
vibe_api_enabled: true,
|
||||
responsive_web_twitter_blue_verified_badge_is_enabled: true,
|
||||
interactive_text_enabled: true,
|
||||
longform_notetweets_richtext_consumption_enabled: true,
|
||||
responsive_web_media_download_video_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildBookmarksFeatures() {
|
||||
return applyFeatureOverrides('bookmarks', {
|
||||
...buildTimelineFeatures(),
|
||||
graphql_timeline_v2_bookmark_timeline: true,
|
||||
});
|
||||
}
|
||||
export function buildLikesFeatures() {
|
||||
return applyFeatureOverrides('likes', buildTimelineFeatures());
|
||||
}
|
||||
export function buildListsFeatures() {
|
||||
return applyFeatureOverrides('lists', {
|
||||
rweb_video_screen_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: true,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_exclude_directive_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_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,
|
||||
responsive_web_grok_show_grok_translated_post: false,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
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,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
blue_business_profile_image_shape_enabled: false,
|
||||
responsive_web_text_conversations_enabled: false,
|
||||
tweetypie_unmention_optimization_enabled: true,
|
||||
vibe_api_enabled: false,
|
||||
interactive_text_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildHomeTimelineFeatures() {
|
||||
return applyFeatureOverrides('homeTimeline', {
|
||||
...buildTimelineFeatures(),
|
||||
});
|
||||
}
|
||||
export function buildUserTweetsFeatures() {
|
||||
return applyFeatureOverrides('userTweets', {
|
||||
rweb_video_screen_enabled: false,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: false,
|
||||
rweb_tipjar_consumption_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,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: true,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_enabled: true,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
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,
|
||||
responsive_web_grok_show_grok_translated_post: true,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
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,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildFollowingFeatures() {
|
||||
return applyFeatureOverrides('following', {
|
||||
rweb_video_screen_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: false,
|
||||
responsive_web_profile_redirect_enabled: true,
|
||||
rweb_tipjar_consumption_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,
|
||||
premium_content_api_read_enabled: true,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
||||
responsive_web_grok_analyze_post_followups_enabled: false,
|
||||
responsive_web_grok_annotations_enabled: false,
|
||||
responsive_web_jetfuel_frame: false,
|
||||
post_ctas_fetch_enabled: true,
|
||||
responsive_web_grok_share_attachment_enabled: false,
|
||||
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: true,
|
||||
responsive_web_grok_show_grok_translated_post: false,
|
||||
responsive_web_grok_analysis_button_from_backend: 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,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: false,
|
||||
responsive_web_grok_imagine_annotation_enabled: false,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
});
|
||||
}
|
||||
export function buildExploreFeatures() {
|
||||
return applyFeatureOverrides('explore', {
|
||||
rweb_video_screen_enabled: true,
|
||||
profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: true,
|
||||
rweb_tipjar_consumption_enabled: true,
|
||||
verified_phone_label_enabled: false,
|
||||
creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_exclude_directive_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false,
|
||||
communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
responsive_web_grok_analyze_button_fetch_trends_enabled: true,
|
||||
responsive_web_grok_analyze_post_followups_enabled: true,
|
||||
responsive_web_grok_annotations_enabled: true,
|
||||
responsive_web_jetfuel_frame: true,
|
||||
responsive_web_grok_share_attachment_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,
|
||||
responsive_web_grok_show_grok_translated_post: true,
|
||||
responsive_web_grok_analysis_button_from_backend: true,
|
||||
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,
|
||||
longform_notetweets_rich_text_read_enabled: true,
|
||||
longform_notetweets_inline_media_enabled: true,
|
||||
responsive_web_grok_image_annotation_enabled: true,
|
||||
responsive_web_grok_imagine_annotation_enabled: true,
|
||||
responsive_web_grok_community_note_auto_translation_is_enabled: true,
|
||||
responsive_web_enhance_cards_enabled: false,
|
||||
// Additional features required for ExploreSidebar
|
||||
post_ctas_fetch_enabled: true,
|
||||
rweb_video_timestamps_enabled: true,
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-features.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { FollowMutationResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientFollowMethods {
|
||||
follow(userId: string): Promise<FollowMutationResult>;
|
||||
unfollow(userId: string): Promise<FollowMutationResult>;
|
||||
}
|
||||
export declare function withFollow<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientFollowMethods>;
|
||||
//# sourceMappingURL=twitter-client-follow.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-follow.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-follow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACzD;AAED,wBAAgB,UAAU,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC7E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,0BAA0B,CAAC,CA8N1C"}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||
export function withFollow(Base) {
|
||||
class TwitterClientFollow extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
/**
|
||||
* Follow a user by their user ID
|
||||
*/
|
||||
async follow(userId) {
|
||||
await this.ensureClientUserId();
|
||||
// Try REST API first (more reliable for follow/unfollow)
|
||||
const restResult = await this.followViaRest(userId, 'create');
|
||||
if (restResult.success) {
|
||||
return restResult;
|
||||
}
|
||||
// Fallback to GraphQL
|
||||
return this.followViaGraphQL(userId, true);
|
||||
}
|
||||
/**
|
||||
* Unfollow a user by their user ID
|
||||
*/
|
||||
async unfollow(userId) {
|
||||
await this.ensureClientUserId();
|
||||
// Try REST API first (more reliable for follow/unfollow)
|
||||
const restResult = await this.followViaRest(userId, 'destroy');
|
||||
if (restResult.success) {
|
||||
return restResult;
|
||||
}
|
||||
// Fallback to GraphQL
|
||||
return this.followViaGraphQL(userId, false);
|
||||
}
|
||||
async followViaRest(userId, action) {
|
||||
const urls = [
|
||||
`https://x.com/i/api/1.1/friendships/${action}.json`,
|
||||
`https://api.twitter.com/1.1/friendships/${action}.json`,
|
||||
];
|
||||
const params = new URLSearchParams({
|
||||
user_id: userId,
|
||||
skip_status: 'true',
|
||||
});
|
||||
let lastError;
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...this.getBaseHeaders(),
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
// Parse error response
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
if (errorData.errors && errorData.errors.length > 0) {
|
||||
const error = errorData.errors[0];
|
||||
// Code 160: Already following / already not following
|
||||
if (error.code === 160) {
|
||||
return { success: true };
|
||||
}
|
||||
// Code 162: Blocked from following
|
||||
if (error.code === 162) {
|
||||
return { success: false, error: 'You have been blocked from following this account' };
|
||||
}
|
||||
// Code 108: Cannot find specified user
|
||||
if (error.code === 108) {
|
||||
return { success: false, error: 'User not found' };
|
||||
}
|
||||
lastError = `${error.message} (code ${error.code})`;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Not JSON, continue with generic error
|
||||
}
|
||||
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
|
||||
continue;
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
lastError = data.errors.map((e) => e.message).join(', ');
|
||||
continue;
|
||||
}
|
||||
// Success - user data returned
|
||||
if (data.id_str || data.screen_name) {
|
||||
return {
|
||||
success: true,
|
||||
userId: data.id_str,
|
||||
username: data.screen_name,
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? `Unknown error during ${action}` };
|
||||
}
|
||||
async followViaGraphQL(userId, follow) {
|
||||
const operationName = follow ? 'CreateFriendship' : 'DestroyFriendship';
|
||||
const variables = {
|
||||
user_id: userId,
|
||||
};
|
||||
const tryOnce = async () => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = await this.getFollowQueryIds(follow);
|
||||
for (const queryId of queryIds) {
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/${operationName}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({ variables, queryId }),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = 'HTTP 404';
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
|
||||
continue;
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
lastError = data.errors.map((e) => e.message).join(', ');
|
||||
continue;
|
||||
}
|
||||
const result = data.data?.user?.result;
|
||||
return {
|
||||
success: true,
|
||||
userId: result?.rest_id,
|
||||
username: result?.legacy?.screen_name,
|
||||
had404,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: lastError ?? `Unknown error during ${operationName}`,
|
||||
had404,
|
||||
};
|
||||
};
|
||||
const firstAttempt = await tryOnce();
|
||||
if (firstAttempt.success) {
|
||||
return { success: true, userId: firstAttempt.userId, username: firstAttempt.username };
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, userId: secondAttempt.userId, username: secondAttempt.username };
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
}
|
||||
async getFollowQueryIds(follow) {
|
||||
const primary = await this.getQueryId(follow ? 'CreateFriendship' : 'DestroyFriendship');
|
||||
const fallbacks = follow
|
||||
? ['8h9JVdV8dlSyqyRDJEPCsA', 'OPwKc1HXnBT_bWXfAlo-9g']
|
||||
: ['ppXWuagMNXgvzx6WoXBW0Q', '8h9JVdV8dlSyqyRDJEPCsA'];
|
||||
return Array.from(new Set([primary, ...fallbacks]));
|
||||
}
|
||||
}
|
||||
return TwitterClientFollow;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-follow.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { SearchResult } from './twitter-client-types.js';
|
||||
/** Options for home timeline fetch methods */
|
||||
export interface HomeTimelineFetchOptions {
|
||||
/** Include raw GraphQL response in `_raw` field */
|
||||
includeRaw?: boolean;
|
||||
}
|
||||
export interface TwitterClientHomeMethods {
|
||||
getHomeTimeline(count?: number, options?: HomeTimelineFetchOptions): Promise<SearchResult>;
|
||||
getHomeLatestTimeline(count?: number, options?: HomeTimelineFetchOptions): Promise<SearchResult>;
|
||||
}
|
||||
export declare function withHome<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientHomeMethods>;
|
||||
//# sourceMappingURL=twitter-client-home.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-home.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-home.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAS7F,8CAA8C;AAC9C,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3F,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAClG;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC3E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CA+KxC"}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||
import { buildHomeTimelineFeatures } from './twitter-client-features.js';
|
||||
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||
const QUERY_UNSPECIFIED_REGEX = /query:\s*unspecified/i;
|
||||
function isQueryIdMismatch(errors) {
|
||||
return errors.some((error) => QUERY_UNSPECIFIED_REGEX.test(error.message ?? ''));
|
||||
}
|
||||
export function withHome(Base) {
|
||||
class TwitterClientHome extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
async getHomeTimelineQueryIds() {
|
||||
const primary = await this.getQueryId('HomeTimeline');
|
||||
return Array.from(new Set([primary, 'edseUwk9sP5Phz__9TIRnA']));
|
||||
}
|
||||
async getHomeLatestTimelineQueryIds() {
|
||||
const primary = await this.getQueryId('HomeLatestTimeline');
|
||||
return Array.from(new Set([primary, 'iOEZpOdfekFsxSlPQCQtPg']));
|
||||
}
|
||||
/**
|
||||
* Get the authenticated user's "For You" home timeline
|
||||
*/
|
||||
async getHomeTimeline(count = 20, options = {}) {
|
||||
return this.fetchHomeTimeline('HomeTimeline', count, options);
|
||||
}
|
||||
/**
|
||||
* Get the authenticated user's "Following" (latest/chronological) home timeline
|
||||
*/
|
||||
async getHomeLatestTimeline(count = 20, options = {}) {
|
||||
return this.fetchHomeTimeline('HomeLatestTimeline', count, options);
|
||||
}
|
||||
async fetchHomeTimeline(operation, count, options) {
|
||||
const { includeRaw = false } = options;
|
||||
const features = buildHomeTimelineFeatures();
|
||||
const pageSize = 20;
|
||||
const seen = new Set();
|
||||
const tweets = [];
|
||||
let cursor;
|
||||
const fetchPage = async (pageCount, pageCursor) => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = operation === 'HomeTimeline'
|
||||
? await this.getHomeTimelineQueryIds()
|
||||
: await this.getHomeLatestTimelineQueryIds();
|
||||
for (const queryId of queryIds) {
|
||||
const variables = {
|
||||
count: pageCount,
|
||||
includePromotedContent: true,
|
||||
latestControlAvailable: true,
|
||||
requestContext: 'launch',
|
||||
withCommunity: true,
|
||||
...(pageCursor ? { cursor: pageCursor } : {}),
|
||||
};
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
features: JSON.stringify(features),
|
||||
});
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/${operation}?${params.toString()}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
const errorMessage = data.errors.map((e) => e.message).join(', ');
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
had404: had404 || isQueryIdMismatch(data.errors),
|
||||
};
|
||||
}
|
||||
const instructions = data.data?.home?.home_timeline_urt?.instructions;
|
||||
const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
|
||||
const nextCursor = extractCursorFromInstructions(instructions);
|
||||
return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? 'Unknown error fetching home timeline', had404 };
|
||||
};
|
||||
const fetchWithRefresh = async (pageCount, pageCursor) => {
|
||||
const firstAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (firstAttempt.success) {
|
||||
return firstAttempt;
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (secondAttempt.success) {
|
||||
return secondAttempt;
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
};
|
||||
while (tweets.length < count) {
|
||||
const pageCount = Math.min(pageSize, count - tweets.length);
|
||||
const page = await fetchWithRefresh(pageCount, cursor);
|
||||
if (!page.success) {
|
||||
return { success: false, error: page.error };
|
||||
}
|
||||
let added = 0;
|
||||
for (const tweet of page.tweets) {
|
||||
if (seen.has(tweet.id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(tweet.id);
|
||||
tweets.push(tweet);
|
||||
added += 1;
|
||||
if (tweets.length >= count) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!page.cursor || page.cursor === cursor || page.tweets.length === 0 || added === 0) {
|
||||
break;
|
||||
}
|
||||
cursor = page.cursor;
|
||||
}
|
||||
return { success: true, tweets };
|
||||
}
|
||||
}
|
||||
return TwitterClientHome;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-home.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { TimelineFetchOptions, TimelinePaginationOptions } from './twitter-client-timelines.js';
|
||||
import type { ListsResult, SearchResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientListMethods {
|
||||
getOwnedLists(count?: number): Promise<ListsResult>;
|
||||
getListMemberships(count?: number): Promise<ListsResult>;
|
||||
getListTimeline(listId: string, count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
|
||||
getAllListTimeline(listId: string, options?: TimelinePaginationOptions): Promise<SearchResult>;
|
||||
}
|
||||
export declare function withLists<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientListMethods>;
|
||||
//# sourceMappingURL=twitter-client-lists.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-lists.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-lists.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AACrG,OAAO,KAAK,EAAsB,WAAW,EAAE,YAAY,EAA0B,MAAM,2BAA2B,CAAC;AAGvH,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACpD,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACzD,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvG,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAChG;AAiFD,wBAAgB,SAAS,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC5E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAyYxC"}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
// ABOUTME: Mixin for Twitter Lists GraphQL operations.
|
||||
// ABOUTME: Provides methods to fetch user's owned lists, memberships, and list timelines.
|
||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||
import { buildListsFeatures } from './twitter-client-features.js';
|
||||
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||
function parseList(listResult) {
|
||||
if (!listResult.id_str || !listResult.name) {
|
||||
return null;
|
||||
}
|
||||
const owner = listResult.user_results?.result;
|
||||
return {
|
||||
id: listResult.id_str,
|
||||
name: listResult.name,
|
||||
description: listResult.description,
|
||||
memberCount: listResult.member_count,
|
||||
subscriberCount: listResult.subscriber_count,
|
||||
isPrivate: listResult.mode?.toLowerCase() === 'private',
|
||||
createdAt: listResult.created_at,
|
||||
owner: owner
|
||||
? {
|
||||
id: owner.rest_id ?? '',
|
||||
username: owner.legacy?.screen_name ?? '',
|
||||
name: owner.legacy?.name ?? '',
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
function parseListsFromInstructions(instructions) {
|
||||
const lists = [];
|
||||
if (!instructions) {
|
||||
return lists;
|
||||
}
|
||||
for (const instruction of instructions) {
|
||||
if (!instruction.entries) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of instruction.entries) {
|
||||
const listResult = entry.content?.itemContent?.list;
|
||||
if (listResult) {
|
||||
const parsed = parseList(listResult);
|
||||
if (parsed) {
|
||||
lists.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lists;
|
||||
}
|
||||
export function withLists(Base) {
|
||||
class TwitterClientLists extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
async getListOwnershipsQueryIds() {
|
||||
const primary = await this.getQueryId('ListOwnerships');
|
||||
return Array.from(new Set([primary, 'wQcOSjSQ8NtgxIwvYl1lMg']));
|
||||
}
|
||||
async getListMembershipsQueryIds() {
|
||||
const primary = await this.getQueryId('ListMemberships');
|
||||
return Array.from(new Set([primary, 'BlEXXdARdSeL_0KyKHHvvg']));
|
||||
}
|
||||
async getListTimelineQueryIds() {
|
||||
const primary = await this.getQueryId('ListLatestTweetsTimeline');
|
||||
return Array.from(new Set([primary, '2TemLyqrMpTeAmysdbnVqw']));
|
||||
}
|
||||
/**
|
||||
* Get lists owned by the authenticated user
|
||||
*/
|
||||
async getOwnedLists(count = 100) {
|
||||
const userResult = await this.getCurrentUser();
|
||||
if (!userResult.success || !userResult.user) {
|
||||
return { success: false, error: userResult.error ?? 'Could not determine current user' };
|
||||
}
|
||||
const variables = {
|
||||
userId: userResult.user.id,
|
||||
count,
|
||||
isListMembershipShown: true,
|
||||
isListMemberTargetUserId: userResult.user.id,
|
||||
};
|
||||
const features = buildListsFeatures();
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
features: JSON.stringify(features),
|
||||
});
|
||||
const tryOnce = async () => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = await this.getListOwnershipsQueryIds();
|
||||
for (const queryId of queryIds) {
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/ListOwnerships?${params.toString()}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 };
|
||||
}
|
||||
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
||||
const lists = parseListsFromInstructions(instructions);
|
||||
return { success: true, lists, had404 };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? 'Unknown error fetching owned lists', had404 };
|
||||
};
|
||||
const firstAttempt = await tryOnce();
|
||||
if (firstAttempt.success) {
|
||||
return { success: true, lists: firstAttempt.lists };
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, lists: secondAttempt.lists };
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
}
|
||||
/**
|
||||
* Get lists the authenticated user is a member of
|
||||
*/
|
||||
async getListMemberships(count = 100) {
|
||||
const userResult = await this.getCurrentUser();
|
||||
if (!userResult.success || !userResult.user) {
|
||||
return { success: false, error: userResult.error ?? 'Could not determine current user' };
|
||||
}
|
||||
const variables = {
|
||||
userId: userResult.user.id,
|
||||
count,
|
||||
isListMembershipShown: true,
|
||||
isListMemberTargetUserId: userResult.user.id,
|
||||
};
|
||||
const features = buildListsFeatures();
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
features: JSON.stringify(features),
|
||||
});
|
||||
const tryOnce = async () => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = await this.getListMembershipsQueryIds();
|
||||
for (const queryId of queryIds) {
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/ListMemberships?${params.toString()}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 };
|
||||
}
|
||||
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
|
||||
const lists = parseListsFromInstructions(instructions);
|
||||
return { success: true, lists, had404 };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? 'Unknown error fetching list memberships', had404 };
|
||||
};
|
||||
const firstAttempt = await tryOnce();
|
||||
if (firstAttempt.success) {
|
||||
return { success: true, lists: firstAttempt.lists };
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, lists: secondAttempt.lists };
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
}
|
||||
/**
|
||||
* Get tweets from a list timeline
|
||||
*/
|
||||
async getListTimeline(listId, count = 20, options = {}) {
|
||||
return this.getListTimelinePaged(listId, count, options);
|
||||
}
|
||||
/**
|
||||
* Get all tweets from a list timeline (paginated)
|
||||
*/
|
||||
async getAllListTimeline(listId, options) {
|
||||
return this.getListTimelinePaged(listId, Number.POSITIVE_INFINITY, options);
|
||||
}
|
||||
/**
|
||||
* Internal paginated list timeline fetcher
|
||||
*/
|
||||
async getListTimelinePaged(listId, limit, options = {}) {
|
||||
const features = buildListsFeatures();
|
||||
const pageSize = 20;
|
||||
const seen = new Set();
|
||||
const tweets = [];
|
||||
let cursor = options.cursor;
|
||||
let nextCursor;
|
||||
let pagesFetched = 0;
|
||||
const { includeRaw = false, maxPages } = options;
|
||||
const fetchPage = async (pageCount, pageCursor) => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = await this.getListTimelineQueryIds();
|
||||
const variables = {
|
||||
listId,
|
||||
count: pageCount,
|
||||
...(pageCursor ? { cursor: pageCursor } : {}),
|
||||
};
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
features: JSON.stringify(features),
|
||||
});
|
||||
for (const queryId of queryIds) {
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/ListLatestTweetsTimeline?${params.toString()}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 };
|
||||
}
|
||||
const instructions = data.data?.list?.tweets_timeline?.timeline?.instructions;
|
||||
const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
|
||||
const nextCursor = extractCursorFromInstructions(instructions);
|
||||
return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? 'Unknown error fetching list timeline', had404 };
|
||||
};
|
||||
const fetchWithRefresh = async (pageCount, pageCursor) => {
|
||||
const firstAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (firstAttempt.success) {
|
||||
return firstAttempt;
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (secondAttempt.success) {
|
||||
return secondAttempt;
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
};
|
||||
const unlimited = limit === Number.POSITIVE_INFINITY;
|
||||
while (unlimited || tweets.length < limit) {
|
||||
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
|
||||
const page = await fetchWithRefresh(pageCount, cursor);
|
||||
if (!page.success) {
|
||||
return { success: false, error: page.error };
|
||||
}
|
||||
pagesFetched += 1;
|
||||
let added = 0;
|
||||
for (const tweet of page.tweets) {
|
||||
if (seen.has(tweet.id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(tweet.id);
|
||||
tweets.push(tweet);
|
||||
added += 1;
|
||||
if (!unlimited && tweets.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const pageCursor = page.cursor;
|
||||
if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
|
||||
nextCursor = undefined;
|
||||
break;
|
||||
}
|
||||
if (maxPages && pagesFetched >= maxPages) {
|
||||
nextCursor = pageCursor;
|
||||
break;
|
||||
}
|
||||
cursor = pageCursor;
|
||||
nextCursor = pageCursor;
|
||||
}
|
||||
return { success: true, tweets, nextCursor };
|
||||
}
|
||||
}
|
||||
return TwitterClientLists;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-lists.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { UploadMediaResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientMediaMethods {
|
||||
uploadMedia(input: {
|
||||
data: Uint8Array;
|
||||
mimeType: string;
|
||||
alt?: string;
|
||||
}): Promise<UploadMediaResult>;
|
||||
}
|
||||
export declare function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientMediaMethods>;
|
||||
//# sourceMappingURL=twitter-client-media.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-media.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-media.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACtG;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC5E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,yBAAyB,CAAC,CAoKzC"}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js';
|
||||
export function withMedia(Base) {
|
||||
class TwitterClientMedia extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
mediaCategoryForMime(mimeType) {
|
||||
if (mimeType.startsWith('image/')) {
|
||||
if (mimeType === 'image/gif') {
|
||||
return 'tweet_gif';
|
||||
}
|
||||
return 'tweet_image';
|
||||
}
|
||||
if (mimeType.startsWith('video/')) {
|
||||
return 'tweet_video';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async uploadMedia(input) {
|
||||
const category = this.mediaCategoryForMime(input.mimeType);
|
||||
if (!category) {
|
||||
return { success: false, error: `Unsupported media type: ${input.mimeType}` };
|
||||
}
|
||||
try {
|
||||
const initParams = new URLSearchParams({
|
||||
command: 'INIT',
|
||||
total_bytes: String(input.data.byteLength),
|
||||
media_type: input.mimeType,
|
||||
media_category: category,
|
||||
});
|
||||
const initResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
headers: this.getUploadHeaders(),
|
||||
body: initParams,
|
||||
});
|
||||
if (!initResp.ok) {
|
||||
const text = await initResp.text();
|
||||
return { success: false, error: `HTTP ${initResp.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const initBody = (await initResp.json());
|
||||
const mediaId = typeof initBody.media_id_string === 'string'
|
||||
? initBody.media_id_string
|
||||
: initBody.media_id !== undefined
|
||||
? String(initBody.media_id)
|
||||
: undefined;
|
||||
if (!mediaId) {
|
||||
return { success: false, error: 'Media upload INIT did not return media_id' };
|
||||
}
|
||||
const chunkSize = 5 * 1024 * 1024;
|
||||
let segmentIndex = 0;
|
||||
for (let offset = 0; offset < input.data.byteLength; offset += chunkSize) {
|
||||
const chunk = input.data.slice(offset, Math.min(input.data.byteLength, offset + chunkSize));
|
||||
const form = new FormData();
|
||||
form.set('command', 'APPEND');
|
||||
form.set('media_id', mediaId);
|
||||
form.set('segment_index', String(segmentIndex));
|
||||
form.set('media', new Blob([chunk], { type: input.mimeType }), 'media');
|
||||
const appendResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
headers: this.getUploadHeaders(),
|
||||
body: form,
|
||||
});
|
||||
if (!appendResp.ok) {
|
||||
const text = await appendResp.text();
|
||||
return { success: false, error: `HTTP ${appendResp.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
segmentIndex += 1;
|
||||
}
|
||||
const finalizeParams = new URLSearchParams({ command: 'FINALIZE', media_id: mediaId });
|
||||
const finalizeResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
headers: this.getUploadHeaders(),
|
||||
body: finalizeParams,
|
||||
});
|
||||
if (!finalizeResp.ok) {
|
||||
const text = await finalizeResp.text();
|
||||
return { success: false, error: `HTTP ${finalizeResp.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const finalizeBody = (await finalizeResp.json());
|
||||
const info = finalizeBody.processing_info;
|
||||
if (info?.state && info.state !== 'succeeded') {
|
||||
let attempts = 0;
|
||||
while (attempts < 20) {
|
||||
if (info.state === 'failed') {
|
||||
const msg = info.error?.message || info.error?.name || 'Media processing failed';
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
const delaySecs = Number.isFinite(info.check_after_secs) ? Math.max(1, info.check_after_secs) : 2;
|
||||
await this.sleep(delaySecs * 1000);
|
||||
const statusUrl = `${TWITTER_UPLOAD_URL}?${new URLSearchParams({
|
||||
command: 'STATUS',
|
||||
media_id: mediaId,
|
||||
}).toString()}`;
|
||||
const statusResp = await this.fetchWithTimeout(statusUrl, {
|
||||
method: 'GET',
|
||||
headers: this.getUploadHeaders(),
|
||||
});
|
||||
if (!statusResp.ok) {
|
||||
const text = await statusResp.text();
|
||||
return { success: false, error: `HTTP ${statusResp.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const statusBody = (await statusResp.json());
|
||||
if (!statusBody.processing_info) {
|
||||
break;
|
||||
}
|
||||
info.state = statusBody.processing_info.state;
|
||||
info.check_after_secs = statusBody.processing_info.check_after_secs;
|
||||
info.error = statusBody.processing_info.error;
|
||||
if (info.state === 'succeeded') {
|
||||
break;
|
||||
}
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
if (input.alt && input.mimeType.startsWith('image/')) {
|
||||
const metaResp = await this.fetchWithTimeout(TWITTER_MEDIA_METADATA_URL, {
|
||||
method: 'POST',
|
||||
headers: this.getJsonHeaders(),
|
||||
body: JSON.stringify({ media_id: mediaId, alt_text: { text: input.alt } }),
|
||||
});
|
||||
if (!metaResp.ok) {
|
||||
const text = await metaResp.text();
|
||||
return { success: false, error: `HTTP ${metaResp.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
}
|
||||
return { success: true, mediaId };
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return TwitterClientMedia;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-media.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { TweetData } from './twitter-client-types.js';
|
||||
declare const TIMELINE_IDS: {
|
||||
readonly forYou: "VGltZWxpbmU6DAC2CwABAAAAB2Zvcl95b3UAAA==";
|
||||
readonly trending: "VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA=";
|
||||
readonly news: "VGltZWxpbmU6DAC2CwABAAAABG5ld3MAAA==";
|
||||
readonly sports: "VGltZWxpbmU6DAC2CwABAAAABnNwb3J0cwAA";
|
||||
readonly entertainment: "VGltZWxpbmU6DAC2CwABAAAADWVudGVydGFpbm1lbnQAAA==";
|
||||
};
|
||||
export type ExploreTab = keyof typeof TIMELINE_IDS;
|
||||
/** Options for news fetch methods */
|
||||
export interface NewsFetchOptions {
|
||||
/** Include raw GraphQL response in `_raw` field */
|
||||
includeRaw?: boolean;
|
||||
/** Also fetch related tweets for each news item */
|
||||
withTweets?: boolean;
|
||||
/** Number of tweets to fetch per news item (default: 5) */
|
||||
tweetsPerItem?: number;
|
||||
/** Filter to show only AI-curated news items */
|
||||
aiOnly?: boolean;
|
||||
/** Fetch from specific tabs only (default: all tabs) */
|
||||
tabs?: ExploreTab[];
|
||||
}
|
||||
export interface NewsItem {
|
||||
id: string;
|
||||
headline: string;
|
||||
category?: string;
|
||||
timeAgo?: string;
|
||||
postCount?: number;
|
||||
description?: string;
|
||||
url?: string;
|
||||
tweets?: TweetData[];
|
||||
_raw?: any;
|
||||
}
|
||||
export type NewsResult = {
|
||||
success: true;
|
||||
items: NewsItem[];
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
export interface TwitterClientNewsMethods {
|
||||
getNews(count?: number, options?: NewsFetchOptions): Promise<NewsResult>;
|
||||
}
|
||||
export declare function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientNewsMethods>;
|
||||
export {};
|
||||
//# sourceMappingURL=twitter-client-news.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-news.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-news.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAMzE,QAAA,MAAM,YAAY;;;;;;CAMR,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,YAAY,CAAC;AAEnD,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IAErB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,MAAM,UAAU,GAClB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1E;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC3E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CA+WxC"}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||
import { buildExploreFeatures } from './twitter-client-features.js';
|
||||
const POST_COUNT_REGEX = /[\d.]+[KMB]?\s*posts?/i;
|
||||
const POST_COUNT_MATCH_REGEX = /([\d.]+)([KMB]?)\s*posts?/i;
|
||||
// Timeline IDs for different Explore tabs
|
||||
const TIMELINE_IDS = {
|
||||
forYou: 'VGltZWxpbmU6DAC2CwABAAAAB2Zvcl95b3UAAA==',
|
||||
trending: 'VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA=',
|
||||
news: 'VGltZWxpbmU6DAC2CwABAAAABG5ld3MAAA==',
|
||||
sports: 'VGltZWxpbmU6DAC2CwABAAAABnNwb3J0cwAA',
|
||||
entertainment: 'VGltZWxpbmU6DAC2CwABAAAADWVudGVydGFpbm1lbnQAAA==',
|
||||
};
|
||||
export function withNews(Base) {
|
||||
class TwitterClientNews extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
/**
|
||||
* Fetch news and trending topics from Twitter's Explore page tabs
|
||||
*/
|
||||
async getNews(count = 10, options = {}) {
|
||||
const { includeRaw = false, withTweets = false, tweetsPerItem = 5, aiOnly = false, tabs = ['forYou', 'news', 'sports', 'entertainment'], } = options;
|
||||
const debug = process.env.BIRD_DEBUG === '1';
|
||||
if (debug) {
|
||||
console.error(`[getNews] Fetching from tabs: ${tabs.join(', ')}`);
|
||||
}
|
||||
const allItems = [];
|
||||
const seenHeadlines = new Set();
|
||||
// Fetch from each tab
|
||||
for (const tab of tabs) {
|
||||
const timelineId = TIMELINE_IDS[tab];
|
||||
if (!timelineId) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const tabItems = await this.fetchTimelineTab(tab, timelineId, count, aiOnly, includeRaw);
|
||||
// Deduplicate across tabs
|
||||
for (const item of tabItems) {
|
||||
if (!seenHeadlines.has(item.headline)) {
|
||||
seenHeadlines.add(item.headline);
|
||||
allItems.push(item);
|
||||
}
|
||||
}
|
||||
if (debug) {
|
||||
console.error(`[getNews] Tab ${tab}: found ${tabItems.length} items, total unique: ${allItems.length}`);
|
||||
}
|
||||
// Stop early if we have enough
|
||||
if (allItems.length >= count) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (debug) {
|
||||
console.error(`[getNews] Error fetching tab ${tab}:`, error);
|
||||
}
|
||||
// Continue with other tabs
|
||||
}
|
||||
}
|
||||
if (allItems.length === 0) {
|
||||
return { success: false, error: 'No news items found' };
|
||||
}
|
||||
// Limit to requested count
|
||||
const items = allItems.slice(0, count);
|
||||
if (withTweets) {
|
||||
await this.enrichWithTweets(items, tweetsPerItem, includeRaw);
|
||||
}
|
||||
return { success: true, items };
|
||||
}
|
||||
/**
|
||||
* Fetch a specific timeline tab using GenericTimelineById
|
||||
*/
|
||||
async fetchTimelineTab(tabName, timelineId, maxCount, aiOnly, includeRaw) {
|
||||
const queryId = await this.getQueryId('GenericTimelineById');
|
||||
const features = buildExploreFeatures();
|
||||
const variables = {
|
||||
timelineId: timelineId,
|
||||
count: maxCount * 2, // Fetch more to account for filtering
|
||||
includePromotedContent: false,
|
||||
};
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
features: JSON.stringify(features),
|
||||
});
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/GenericTimelineById?${params.toString()}`;
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
|
||||
}
|
||||
const data = (await response.json());
|
||||
// Debug: save response if BIRD_DEBUG_JSON is set
|
||||
if (process.env.BIRD_DEBUG_JSON) {
|
||||
const fs = await import('node:fs/promises');
|
||||
const debugPath = process.env.BIRD_DEBUG_JSON.replace('.json', `-${tabName}.json`);
|
||||
await fs.writeFile(debugPath, JSON.stringify(data, null, 2)).catch(() => { });
|
||||
}
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
throw new Error(data.errors.map((e) => e.message).join('; '));
|
||||
}
|
||||
// Parse timeline response
|
||||
return this.parseTimelineTabItems(data, tabName, maxCount, aiOnly, includeRaw);
|
||||
}
|
||||
/**
|
||||
* Parse items from a GenericTimelineById response
|
||||
*/
|
||||
parseTimelineTabItems(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: API response structure is complex
|
||||
data, source, maxCount, aiOnly, includeRaw) {
|
||||
const items = [];
|
||||
const seenHeadlines = new Set();
|
||||
// Navigate to timeline instructions
|
||||
const timeline = data?.data?.timeline?.timeline;
|
||||
if (!timeline) {
|
||||
return [];
|
||||
}
|
||||
const instructions = timeline.instructions || [];
|
||||
for (const instruction of instructions) {
|
||||
const entries = instruction.entries ?? (instruction.entry ? [instruction.entry] : []);
|
||||
if (!entries || entries.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (items.length >= maxCount) {
|
||||
break;
|
||||
}
|
||||
const content = entry.content;
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
// Handle TimelineTimelineItem (single trend item)
|
||||
if (content.itemContent) {
|
||||
const newsItem = this.parseNewsItemFromContent(content.itemContent, entry.entryId, source, seenHeadlines, aiOnly, includeRaw);
|
||||
if (newsItem) {
|
||||
items.push(newsItem);
|
||||
}
|
||||
}
|
||||
// Handle TimelineTimelineModule (multiple items)
|
||||
const itemsArray = content?.items || [];
|
||||
for (const data of itemsArray) {
|
||||
if (items.length >= maxCount) {
|
||||
break;
|
||||
}
|
||||
// Structure can be data.itemContent OR data.item.itemContent
|
||||
const itemContent = data?.itemContent || data?.item?.itemContent;
|
||||
if (!itemContent) {
|
||||
continue;
|
||||
}
|
||||
const newsItem = this.parseNewsItemFromContent(itemContent, entry.entryId, source, seenHeadlines, aiOnly, includeRaw);
|
||||
if (newsItem) {
|
||||
items.push(newsItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
parseNewsItemFromContent(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: API response structure is complex
|
||||
itemContent, entryId, source, seenHeadlines, aiOnly, includeRaw) {
|
||||
const headline = itemContent.name || itemContent.title;
|
||||
if (!headline) {
|
||||
return null;
|
||||
}
|
||||
const trendMetadata = itemContent?.trend_metadata;
|
||||
const trendUrl = itemContent.trend_url?.url || trendMetadata?.url?.url;
|
||||
// Detect AI news by characteristics:
|
||||
// 1. Full sentence headlines (contains spaces and is longer)
|
||||
// 2. Has social_context with "News" category
|
||||
// 3. Or explicitly marked as is_ai_trend
|
||||
const socialContext = itemContent?.social_context?.text || '';
|
||||
const hasNewsCategory = socialContext.includes('News') || socialContext.includes('hours ago');
|
||||
const isFullSentence = headline.split(' ').length >= 5; // AI news are full sentences
|
||||
const isExplicitlyAiTrend = itemContent.is_ai_trend === true;
|
||||
const isAiNews = isExplicitlyAiTrend || (isFullSentence && hasNewsCategory);
|
||||
// Filter AI trends if aiOnly is enabled
|
||||
if (aiOnly && !isAiNews) {
|
||||
return null;
|
||||
}
|
||||
if (seenHeadlines.has(headline)) {
|
||||
return null;
|
||||
}
|
||||
seenHeadlines.add(headline);
|
||||
let postCount;
|
||||
let timeAgo;
|
||||
let category = 'Trending';
|
||||
// Parse social context for metadata
|
||||
const socialCtx = itemContent?.social_context;
|
||||
if (socialCtx?.text) {
|
||||
const socialContextText = socialCtx.text;
|
||||
const parts = socialContextText.split('·').map((s) => s.trim());
|
||||
for (const part of parts) {
|
||||
if (part.includes('ago')) {
|
||||
timeAgo = part;
|
||||
}
|
||||
else if (part.match(POST_COUNT_REGEX)) {
|
||||
const match = part.match(POST_COUNT_MATCH_REGEX);
|
||||
if (match) {
|
||||
let num = Number.parseFloat(match[1]);
|
||||
const suffix = match[2]?.toUpperCase();
|
||||
if (suffix === 'K') {
|
||||
num *= 1000;
|
||||
}
|
||||
else if (suffix === 'M') {
|
||||
num *= 1_000_000;
|
||||
}
|
||||
else if (suffix === 'B') {
|
||||
num *= 1_000_000_000;
|
||||
}
|
||||
postCount = Math.round(num);
|
||||
}
|
||||
}
|
||||
else {
|
||||
category = part;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse trend metadata
|
||||
if (trendMetadata?.meta_description) {
|
||||
const metaDesc = trendMetadata.meta_description;
|
||||
const postMatch = metaDesc.match(POST_COUNT_MATCH_REGEX);
|
||||
if (postMatch) {
|
||||
let num = Number.parseFloat(postMatch[1]);
|
||||
const suffix = postMatch[2]?.toUpperCase();
|
||||
if (suffix === 'K') {
|
||||
num *= 1000;
|
||||
}
|
||||
else if (suffix === 'M') {
|
||||
num *= 1_000_000;
|
||||
}
|
||||
else if (suffix === 'B') {
|
||||
num *= 1_000_000_000;
|
||||
}
|
||||
postCount = Math.round(num);
|
||||
}
|
||||
}
|
||||
if (trendMetadata?.domain_context && (category === 'Trending' || category === 'News')) {
|
||||
category = trendMetadata.domain_context;
|
||||
}
|
||||
const item = {
|
||||
id: trendUrl ?? (entryId ? `${entryId}-${headline}` : `${source}-${headline}`),
|
||||
headline,
|
||||
category: isAiNews ? `AI · ${category}` : category,
|
||||
timeAgo,
|
||||
postCount,
|
||||
description: itemContent.description,
|
||||
url: trendUrl,
|
||||
};
|
||||
if (includeRaw) {
|
||||
item._raw = itemContent;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
async enrichWithTweets(items, tweetsPerItem, includeRaw) {
|
||||
const debug = process.env.BIRD_DEBUG === '1';
|
||||
for (const item of items) {
|
||||
try {
|
||||
const searchQuery = item.headline;
|
||||
if (!searchQuery) {
|
||||
continue;
|
||||
}
|
||||
// Use the search method if available (requires search mixin)
|
||||
if ('search' in this && typeof this.search === 'function') {
|
||||
const result = (await this.search(searchQuery, tweetsPerItem, { includeRaw }));
|
||||
if (result.success && result.tweets) {
|
||||
item.tweets = result.tweets;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if (debug) {
|
||||
console.error('[getNews] Failed to enrich item with tweets:', item.headline);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return TwitterClientNews;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-news.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { TweetResult } from './twitter-client-types.js';
|
||||
export interface TwitterClientPostingMethods {
|
||||
tweet(text: string, mediaIds?: string[]): Promise<TweetResult>;
|
||||
reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise<TweetResult>;
|
||||
}
|
||||
export declare function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientPostingMethods>;
|
||||
//# sourceMappingURL=twitter-client-posting.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-posting.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-posting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAuB,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAElF,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxF;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC9E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAkR3C"}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js';
|
||||
import { buildTweetCreateFeatures } from './twitter-client-features.js';
|
||||
export function withPosting(Base) {
|
||||
class TwitterClientPosting extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
/**
|
||||
* Post a new tweet
|
||||
*/
|
||||
async tweet(text, mediaIds) {
|
||||
const variables = {
|
||||
tweet_text: text,
|
||||
dark_request: false,
|
||||
media: {
|
||||
media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })),
|
||||
possibly_sensitive: false,
|
||||
},
|
||||
semantic_annotation_ids: [],
|
||||
};
|
||||
const features = buildTweetCreateFeatures();
|
||||
return this.createTweet(variables, features);
|
||||
}
|
||||
/**
|
||||
* Reply to an existing tweet
|
||||
*/
|
||||
async reply(text, replyToTweetId, mediaIds) {
|
||||
const variables = {
|
||||
tweet_text: text,
|
||||
reply: {
|
||||
in_reply_to_tweet_id: replyToTweetId,
|
||||
exclude_reply_user_ids: [],
|
||||
},
|
||||
dark_request: false,
|
||||
media: {
|
||||
media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })),
|
||||
possibly_sensitive: false,
|
||||
},
|
||||
semantic_annotation_ids: [],
|
||||
};
|
||||
const features = buildTweetCreateFeatures();
|
||||
return this.createTweet(variables, features);
|
||||
}
|
||||
async createTweet(variables, features) {
|
||||
await this.ensureClientUserId();
|
||||
let queryId = await this.getQueryId('CreateTweet');
|
||||
let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`;
|
||||
const buildBody = () => JSON.stringify({ variables, features, queryId });
|
||||
let body = buildBody();
|
||||
try {
|
||||
const headers = { ...this.getHeaders(), referer: 'https://x.com/compose/post' };
|
||||
let response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
// Twitter increasingly prefers POST to /i/api/graphql with queryId in the payload.
|
||||
// If the operation URL 404s, retry the generic endpoint.
|
||||
if (response.status === 404) {
|
||||
await this.refreshQueryIds();
|
||||
queryId = await this.getQueryId('CreateTweet');
|
||||
urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`;
|
||||
body = buildBody();
|
||||
response = await this.fetchWithTimeout(urlWithOperation, {
|
||||
method: 'POST',
|
||||
headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' },
|
||||
body,
|
||||
});
|
||||
if (response.status === 404) {
|
||||
const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, {
|
||||
method: 'POST',
|
||||
headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' },
|
||||
body,
|
||||
});
|
||||
if (!retry.ok) {
|
||||
const text = await retry.text();
|
||||
return { success: false, error: `HTTP ${retry.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const data = (await retry.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
const fallback = await this.tryStatusUpdateFallback(data.errors, variables);
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
return { success: false, error: this.formatErrors(data.errors) };
|
||||
}
|
||||
const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id;
|
||||
if (tweetId) {
|
||||
return { success: true, tweetId };
|
||||
}
|
||||
return { success: false, error: 'Tweet created but no ID returned' };
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
|
||||
};
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
const fallback = await this.tryStatusUpdateFallback(data.errors, variables);
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: this.formatErrors(data.errors),
|
||||
};
|
||||
}
|
||||
const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id;
|
||||
if (tweetId) {
|
||||
return {
|
||||
success: true,
|
||||
tweetId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: 'Tweet created but no ID returned',
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
formatErrors(errors) {
|
||||
return errors
|
||||
.map((error) => (typeof error.code === 'number' ? `${error.message} (${error.code})` : error.message))
|
||||
.join(', ');
|
||||
}
|
||||
statusUpdateInputFromCreateTweetVariables(variables) {
|
||||
const text = typeof variables.tweet_text === 'string' ? variables.tweet_text : null;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const reply = variables.reply;
|
||||
const inReplyToTweetId = reply &&
|
||||
typeof reply === 'object' &&
|
||||
typeof reply.in_reply_to_tweet_id === 'string'
|
||||
? reply.in_reply_to_tweet_id
|
||||
: undefined;
|
||||
const media = variables.media;
|
||||
const mediaEntities = media && typeof media === 'object' ? media.media_entities : undefined;
|
||||
const mediaIds = Array.isArray(mediaEntities)
|
||||
? mediaEntities
|
||||
.map((entity) => entity && typeof entity === 'object' && 'media_id' in entity
|
||||
? entity.media_id
|
||||
: undefined)
|
||||
.filter((value) => typeof value === 'string' || typeof value === 'number')
|
||||
.map((value) => String(value))
|
||||
: undefined;
|
||||
return { text, inReplyToTweetId, mediaIds: mediaIds && mediaIds.length > 0 ? mediaIds : undefined };
|
||||
}
|
||||
async postStatusUpdate(input) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('status', input.text);
|
||||
if (input.inReplyToTweetId) {
|
||||
params.set('in_reply_to_status_id', input.inReplyToTweetId);
|
||||
params.set('auto_populate_reply_metadata', 'true');
|
||||
}
|
||||
if (input.mediaIds && input.mediaIds.length > 0) {
|
||||
params.set('media_ids', input.mediaIds.join(','));
|
||||
}
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(TWITTER_STATUS_UPDATE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...this.getBaseHeaders(),
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
referer: 'https://x.com/compose/post',
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
return { success: false, error: this.formatErrors(data.errors) };
|
||||
}
|
||||
const tweetId = typeof data.id_str === 'string' ? data.id_str : data.id !== undefined ? String(data.id) : undefined;
|
||||
if (tweetId) {
|
||||
return { success: true, tweetId };
|
||||
}
|
||||
return { success: false, error: 'Tweet created but no ID returned' };
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
async tryStatusUpdateFallback(errors, variables) {
|
||||
if (!errors.some((error) => error.code === 226)) {
|
||||
return null;
|
||||
}
|
||||
const input = this.statusUpdateInputFromCreateTweetVariables(variables);
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
const fallback = await this.postStatusUpdate(input);
|
||||
if (fallback.success) {
|
||||
return fallback;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return TwitterClientPosting;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-posting.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { SearchResult } from './twitter-client-types.js';
|
||||
/** Options for search methods */
|
||||
export interface SearchFetchOptions {
|
||||
/** Include raw GraphQL response in `_raw` field */
|
||||
includeRaw?: boolean;
|
||||
}
|
||||
/** Options for paged search methods */
|
||||
export interface SearchPaginationOptions extends SearchFetchOptions {
|
||||
maxPages?: number;
|
||||
/** Starting cursor for pagination (resume from previous fetch) */
|
||||
cursor?: string;
|
||||
}
|
||||
export interface TwitterClientSearchMethods {
|
||||
search(query: string, count?: number, options?: SearchFetchOptions): Promise<SearchResult>;
|
||||
getAllSearchResults(query: string, options?: SearchPaginationOptions): Promise<SearchResult>;
|
||||
}
|
||||
export declare function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientSearchMethods>;
|
||||
//# sourceMappingURL=twitter-client-search.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-search.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAKzE,iCAAiC;AACjC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,uCAAuC;AACvC,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3F,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC9F;AAuBD,wBAAgB,UAAU,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC7E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,0BAA0B,CAAC,CA0M1C"}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||
import { buildSearchFeatures } from './twitter-client-features.js';
|
||||
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||
const RAW_QUERY_MISSING_REGEX = /must be defined/i;
|
||||
function isQueryIdMismatch(payload) {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
return (parsed.errors?.some((error) => {
|
||||
if (error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED') {
|
||||
return true;
|
||||
}
|
||||
if (error?.path?.includes('rawQuery') && RAW_QUERY_MISSING_REGEX.test(error.message ?? '')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}) ?? false);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export function withSearch(Base) {
|
||||
class TwitterClientSearch extends Base {
|
||||
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
/**
|
||||
* Search for tweets matching a query
|
||||
*/
|
||||
async search(query, count = 20, options = {}) {
|
||||
return this.searchPaged(query, count, options);
|
||||
}
|
||||
/**
|
||||
* Get all search results (paged)
|
||||
*/
|
||||
async getAllSearchResults(query, options) {
|
||||
return this.searchPaged(query, Number.POSITIVE_INFINITY, options);
|
||||
}
|
||||
async searchPaged(query, limit, options = {}) {
|
||||
const features = buildSearchFeatures();
|
||||
const pageSize = 20;
|
||||
const seen = new Set();
|
||||
const tweets = [];
|
||||
let cursor = options.cursor;
|
||||
let nextCursor;
|
||||
let pagesFetched = 0;
|
||||
const { includeRaw = false, maxPages } = options;
|
||||
const fetchPage = async (pageCount, pageCursor) => {
|
||||
let lastError;
|
||||
let had404 = false;
|
||||
const queryIds = await this.getSearchTimelineQueryIds();
|
||||
for (const queryId of queryIds) {
|
||||
const variables = {
|
||||
rawQuery: query,
|
||||
count: pageCount,
|
||||
querySource: 'typed_query',
|
||||
product: 'Latest',
|
||||
...(pageCursor ? { cursor: pageCursor } : {}),
|
||||
};
|
||||
const params = new URLSearchParams({
|
||||
variables: JSON.stringify(variables),
|
||||
});
|
||||
const url = `${TWITTER_API_BASE}/${queryId}/SearchTimeline?${params.toString()}`;
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({ features, queryId }),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
had404 = true;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const shouldRefreshQueryIds = (response.status === 400 || response.status === 422) && isQueryIdMismatch(text);
|
||||
return {
|
||||
success: false,
|
||||
error: `HTTP ${response.status}: ${text.slice(0, 200)}`,
|
||||
had404: had404 || shouldRefreshQueryIds,
|
||||
};
|
||||
}
|
||||
const data = (await response.json());
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
const shouldRefreshQueryIds = data.errors.some((error) => error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED');
|
||||
return {
|
||||
success: false,
|
||||
error: data.errors.map((e) => e.message).join(', '),
|
||||
had404: had404 || shouldRefreshQueryIds,
|
||||
};
|
||||
}
|
||||
const instructions = data.data?.search_by_raw_query?.search_timeline?.timeline?.instructions;
|
||||
const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
|
||||
const nextCursor = extractCursorFromInstructions(instructions);
|
||||
return { success: true, tweets: pageTweets, cursor: nextCursor, had404 };
|
||||
}
|
||||
catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
return { success: false, error: lastError ?? 'Unknown error fetching search results', had404 };
|
||||
};
|
||||
const fetchWithRefresh = async (pageCount, pageCursor) => {
|
||||
const firstAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (firstAttempt.success) {
|
||||
return firstAttempt;
|
||||
}
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await fetchPage(pageCount, pageCursor);
|
||||
if (secondAttempt.success) {
|
||||
return secondAttempt;
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
return { success: false, error: firstAttempt.error };
|
||||
};
|
||||
const unlimited = limit === Number.POSITIVE_INFINITY;
|
||||
while (unlimited || tweets.length < limit) {
|
||||
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
|
||||
const page = await fetchWithRefresh(pageCount, cursor);
|
||||
if (!page.success) {
|
||||
return { success: false, error: page.error };
|
||||
}
|
||||
pagesFetched += 1;
|
||||
let added = 0;
|
||||
for (const tweet of page.tweets) {
|
||||
if (seen.has(tweet.id)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(tweet.id);
|
||||
tweets.push(tweet);
|
||||
added += 1;
|
||||
if (!unlimited && tweets.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const pageCursor = page.cursor;
|
||||
if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
|
||||
nextCursor = undefined;
|
||||
break;
|
||||
}
|
||||
if (maxPages && pagesFetched >= maxPages) {
|
||||
nextCursor = pageCursor;
|
||||
break;
|
||||
}
|
||||
cursor = pageCursor;
|
||||
nextCursor = pageCursor;
|
||||
}
|
||||
return { success: true, tweets, nextCursor };
|
||||
}
|
||||
}
|
||||
return TwitterClientSearch;
|
||||
}
|
||||
//# sourceMappingURL=twitter-client-search.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
|
||||
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||
import type { SearchResult } from './twitter-client-types.js';
|
||||
/** Options for timeline fetch methods */
|
||||
export interface TimelineFetchOptions {
|
||||
/** Include raw GraphQL response in `_raw` field */
|
||||
includeRaw?: boolean;
|
||||
}
|
||||
/** Options for paged timeline fetch methods */
|
||||
export interface TimelinePaginationOptions extends TimelineFetchOptions {
|
||||
maxPages?: number;
|
||||
/** Starting cursor for pagination (resume from previous fetch) */
|
||||
cursor?: string;
|
||||
}
|
||||
export interface TwitterClientTimelineMethods {
|
||||
getBookmarks(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
|
||||
getAllBookmarks(options?: TimelinePaginationOptions): Promise<SearchResult>;
|
||||
getLikes(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
|
||||
getAllLikes(options?: TimelinePaginationOptions): Promise<SearchResult>;
|
||||
getBookmarkFolderTimeline(folderId: string, count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
|
||||
getAllBookmarkFolderTimeline(folderId: string, options?: TimelinePaginationOptions): Promise<SearchResult>;
|
||||
}
|
||||
export declare function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase): Mixin<TBase, TwitterClientTimelineMethods>;
|
||||
//# sourceMappingURL=twitter-client-timelines.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"twitter-client-timelines.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-timelines.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAG7F,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,+CAA+C;AAC/C,MAAM,WAAW,yBAA0B,SAAQ,oBAAoB;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpF,eAAe,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,WAAW,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACnH,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5G;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAChF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,4BAA4B,CAAC,CA4lB5C"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user