shared.js
1 const QUERY_ID_PATTERN = /^[A-Za-z0-9_-]+$/; 2 export function sanitizeQueryId(resolved, fallbackId) { 3 return typeof resolved === 'string' && QUERY_ID_PATTERN.test(resolved) ? resolved : fallbackId; 4 } 5 export async function resolveTwitterQueryId(page, operationName, fallbackId) { 6 const resolved = await page.evaluate(`async () => { 7 const operationName = ${JSON.stringify(operationName)}; 8 const controller = new AbortController(); 9 const timeout = setTimeout(() => controller.abort(), 5000); 10 try { 11 const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json', { signal: controller.signal }); 12 clearTimeout(timeout); 13 if (ghResp.ok) { 14 const data = await ghResp.json(); 15 const entry = data?.[operationName]; 16 if (entry && entry.queryId) return entry.queryId; 17 } 18 } catch { 19 clearTimeout(timeout); 20 } 21 try { 22 const scripts = performance.getEntriesByType('resource') 23 .filter(r => r.name.includes('client-web') && r.name.endsWith('.js')) 24 .map(r => r.name); 25 for (const scriptUrl of scripts.slice(0, 15)) { 26 try { 27 const text = await (await fetch(scriptUrl)).text(); 28 const re = new RegExp('queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"' + operationName + '"'); 29 const match = text.match(re); 30 if (match) return match[1]; 31 } catch {} 32 } 33 } catch {} 34 return null; 35 }`); 36 return sanitizeQueryId(resolved, fallbackId); 37 } 38 /** 39 * Extract media flags and URLs from a tweet's `legacy` object. 40 * 41 * Prefers `extended_entities.media` (superset with full video_info) and falls 42 * back to `entities.media` when the extended form is missing. For videos and 43 * animated GIFs, returns the mp4 variant URL; for photos, returns 44 * `media_url_https`. 45 */ 46 export function extractMedia(legacy) { 47 const media = legacy?.extended_entities?.media || legacy?.entities?.media; 48 if (!Array.isArray(media) || media.length === 0) { 49 return { has_media: false, media_urls: [] }; 50 } 51 const urls = []; 52 for (const m of media) { 53 if (!m) continue; 54 if (m.type === 'video' || m.type === 'animated_gif') { 55 const variants = m.video_info?.variants || []; 56 const mp4 = variants.find((v) => v?.content_type === 'video/mp4'); 57 const url = mp4?.url || m.media_url_https; 58 if (url) urls.push(url); 59 } else { 60 if (m.media_url_https) urls.push(m.media_url_https); 61 } 62 } 63 return { has_media: urls.length > 0, media_urls: urls }; 64 } 65 export const __test__ = { 66 sanitizeQueryId, 67 extractMedia, 68 };