Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Fetch any URL via Chrome CDP and convert the rendered page to clean markdown with YouTube transcript support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/lib/adapters/x/shared.ts
1import path from "node:path";2import type { NetworkEntry } from "../../browser/network-journal";3import type { XMedia, XQuotedTweet, XTweet, XUser, JsonObject } from "./types";45const X_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "webp", "gif", "bmp", "avif"]);67function emptyObject(): JsonObject {8return {};9}1011export function isRecord(value: unknown): value is JsonObject {12return Boolean(value) && typeof value === "object" && !Array.isArray(value);13}1415export function walk(value: unknown, visitor: (node: unknown) => boolean | void): boolean {16if (visitor(value)) {17return true;18}1920if (Array.isArray(value)) {21for (const item of value) {22if (walk(item, visitor)) {23return true;24}25}26return false;27}2829if (isRecord(value)) {30for (const child of Object.values(value)) {31if (walk(child, visitor)) {32return true;33}34}35}3637return false;38}3940function hasTweetText(node: JsonObject): boolean {41const legacy = isRecord(node.legacy) ? node.legacy : emptyObject();42return (43typeof legacy.full_text === "string" ||44typeof getNoteTweetText(node) === "string"45);46}4748export function findTweetNodeById(payload: unknown, tweetId: string): JsonObject | null {49let match: JsonObject | null = null;5051walk(payload, (node) => {52if (!isRecord(node) || typeof node.rest_id !== "string" || !isRecord(node.legacy)) {53return false;54}5556if (!hasTweetText(node)) {57return false;58}5960if (node.rest_id === tweetId) {61match = node;62return true;63}6465return false;66});6768return match;69}7071export function findTweetNode(payload: unknown, statusId: string): JsonObject | null {72let firstMatch: JsonObject | null = null;73const exactMatch = findTweetNodeById(payload, statusId);74if (exactMatch) {75return exactMatch;76}7778walk(payload, (node) => {79if (!isRecord(node) || typeof node.rest_id !== "string" || !isRecord(node.legacy)) {80return false;81}82if (!hasTweetText(node)) {83return false;84}85if (!firstMatch) {86firstMatch = node;87}88return false;89});9091return firstMatch;92}9394export function getLegacy(tweet: JsonObject): JsonObject {95return isRecord(tweet.legacy) ? tweet.legacy : emptyObject();96}9798export function unwrapTweetResult(node: unknown): JsonObject | null {99if (!isRecord(node)) {100return null;101}102103if (node.__typename === "TweetWithVisibilityResults" && isRecord(node.tweet)) {104return unwrapTweetResult(node.tweet);105}106107const tweet = isRecord(node.tweet) ? (node.tweet as JsonObject) : node;108if (typeof tweet.rest_id !== "string" || !isRecord(tweet.legacy)) {109return null;110}111112return tweet;113}114115export function getUser(tweet: JsonObject): XUser {116const result =117isRecord(tweet.core) &&118isRecord(tweet.core.user_results) &&119isRecord(tweet.core.user_results.result)120? (tweet.core.user_results.result as JsonObject)121: emptyObject();122const legacy = isRecord(result.legacy) ? result.legacy : emptyObject();123const core = isRecord(result.core) ? result.core : emptyObject();124return {125name:126(typeof legacy.name === "string" ? legacy.name : undefined) ??127(typeof core.name === "string" ? core.name : undefined),128screenName:129(typeof legacy.screen_name === "string" ? legacy.screen_name : undefined) ??130(typeof core.screen_name === "string" ? core.screen_name : undefined),131};132}133134function getNoteTweetResult(tweet: JsonObject): JsonObject | null {135if (136!isRecord(tweet.note_tweet) ||137!isRecord(tweet.note_tweet.note_tweet_results) ||138!isRecord(tweet.note_tweet.note_tweet_results.result)139) {140return null;141}142143return tweet.note_tweet.note_tweet_results.result as JsonObject;144}145146function getNoteTweetText(tweet: JsonObject): string | undefined {147const noteTweet = getNoteTweetResult(tweet);148return typeof noteTweet?.text === "string" ? noteTweet.text : undefined;149}150151interface TweetUrlEntity {152url: string;153expandedUrl?: string;154displayUrl?: string;155}156157function collectTweetUrlEntities(values: unknown[]): TweetUrlEntity[] {158return values.reduce<TweetUrlEntity[]>((entities, value) => {159if (!isRecord(value) || typeof value.url !== "string" || !value.url) {160return entities;161}162163entities.push({164url: value.url,165expandedUrl: typeof value.expanded_url === "string" ? value.expanded_url : undefined,166displayUrl: typeof value.display_url === "string" ? value.display_url : undefined,167});168169return entities;170}, []);171}172173function getTweetUrlEntities(tweet: JsonObject): TweetUrlEntity[] {174const noteTweet = getNoteTweetResult(tweet);175const noteTweetEntitySet = noteTweet && isRecord(noteTweet.entity_set) ? noteTweet.entity_set : emptyObject();176const noteTweetUrls = collectTweetUrlEntities(Array.isArray(noteTweetEntitySet.urls) ? noteTweetEntitySet.urls : []);177178const legacy = getLegacy(tweet);179const legacyEntities = isRecord(legacy.entities) ? legacy.entities : emptyObject();180const legacyUrls = collectTweetUrlEntities(Array.isArray(legacyEntities.urls) ? legacyEntities.urls : []);181182const seen = new Set<string>();183return [...noteTweetUrls, ...legacyUrls].filter((value) => {184if (seen.has(value.url)) {185return false;186}187seen.add(value.url);188return true;189});190}191192export function getTweetText(tweet: JsonObject): string {193const legacy = getLegacy(tweet);194let text =195getNoteTweetText(tweet) ?? (typeof legacy.full_text === "string" ? legacy.full_text : "");196197for (const value of getTweetUrlEntities(tweet)) {198const replacement =199(typeof value.expandedUrl === "string" && value.expandedUrl) ||200(typeof value.displayUrl === "string" && value.displayUrl) ||201value.url;202text = text.replaceAll(value.url, replacement);203}204205const extendedEntities = isRecord(legacy.extended_entities) ? legacy.extended_entities : emptyObject();206const media = Array.isArray(extendedEntities.media) ? extendedEntities.media : [];207for (const value of media) {208if (isRecord(value) && typeof value.url === "string") {209text = text.replaceAll(value.url, "").trim();210}211}212213return text.replace(/\n{3,}/g, "\n\n").trim();214}215216function normalizeXImageExtension(raw: string | undefined | null): string | undefined {217if (!raw) {218return undefined;219}220221const normalized = raw.replace(/^\./, "").trim().toLowerCase();222if (!normalized) {223return undefined;224}225226return normalized === "jpeg" ? "jpg" : normalized;227}228229export function toHighResXImageUrl(rawUrl: string): string {230try {231const parsed = new URL(rawUrl);232if (parsed.hostname.toLowerCase() !== "pbs.twimg.com") {233return rawUrl;234}235236const pathExtension = normalizeXImageExtension(path.posix.extname(parsed.pathname));237const format = normalizeXImageExtension(parsed.searchParams.get("format")) ?? pathExtension;238if (!format || !X_IMAGE_EXTENSIONS.has(format)) {239return rawUrl;240}241242if (pathExtension) {243parsed.pathname = parsed.pathname.replace(new RegExp(`\\.${pathExtension}$`, "i"), "");244}245246parsed.searchParams.set("format", format);247parsed.searchParams.set("name", "4096x4096");248return parsed.toString();249} catch {250return rawUrl;251}252}253254function getVideoVariantBitrate(variant: JsonObject): number {255const value = variant.bitrate ?? variant.bit_rate;256return typeof value === "number" && Number.isFinite(value) ? value : 0;257}258259function getVideoVariantContentType(variant: JsonObject): string {260const value = variant.content_type ?? variant.contentType;261return typeof value === "string" ? value.toLowerCase() : "";262}263264export function resolveBestXVideoVariantUrl(mediaInfo: unknown): string | undefined {265if (!isRecord(mediaInfo)) {266return undefined;267}268269const variantsSource =270Array.isArray(mediaInfo.variants)271? mediaInfo.variants272: isRecord(mediaInfo.video_info) && Array.isArray(mediaInfo.video_info.variants)273? mediaInfo.video_info.variants274: [];275276const variants = variantsSource277.filter(278(variant): variant is JsonObject =>279isRecord(variant) && typeof variant.url === "string" && variant.url.length > 0,280)281.filter((variant) => getVideoVariantContentType(variant) === "video/mp4")282.sort((left, right) => getVideoVariantBitrate(right) - getVideoVariantBitrate(left));283284return typeof variants[0]?.url === "string" ? variants[0].url : undefined;285}286287export function getTweetMedia(tweet: JsonObject): XMedia[] {288const legacy = getLegacy(tweet);289const extendedEntities = isRecord(legacy.extended_entities) ? legacy.extended_entities : emptyObject();290const media = Array.isArray(extendedEntities.media) ? extendedEntities.media : [];291292return media293.map((value) => {294if (!isRecord(value) || typeof value.type !== "string") {295return null;296}297if (value.type === "photo" && typeof value.media_url_https === "string") {298return {299type: value.type,300url: toHighResXImageUrl(value.media_url_https),301alt: typeof value.ext_alt_text === "string" ? value.ext_alt_text : undefined,302};303}304if (value.type === "video" || value.type === "animated_gif") {305const videoUrl = resolveBestXVideoVariantUrl(value);306if (!videoUrl) {307return null;308}309return {310type: value.type,311url: videoUrl,312};313}314return null;315})316.filter((value): value is XMedia => value !== null);317}318319export function getTweetUrl(tweet: JsonObject, fallbackUrl: string): string {320const user = getUser(tweet);321const fallbackScreenName = extractScreenNameFromUrl(fallbackUrl);322const id = typeof tweet.rest_id === "string" ? tweet.rest_id : "";323const screenName = user.screenName ?? fallbackScreenName;324if (screenName && id) {325return `https://x.com/${screenName}/status/${id}`;326}327return fallbackUrl;328}329330export function getQuotedTweet(tweet: JsonObject, fallbackUrl: string): XQuotedTweet | undefined {331const quoted = unwrapTweetResult(332isRecord(tweet.quoted_status_result) ? tweet.quoted_status_result.result : null,333);334if (!quoted) {335return undefined;336}337338const user = getUser(quoted);339return {340id: typeof quoted.rest_id === "string" ? quoted.rest_id : "",341author: user.screenName,342authorName: user.name,343text: getTweetText(quoted),344url: getTweetUrl(quoted, fallbackUrl),345media: getTweetMedia(quoted),346};347}348349export function extractScreenNameFromUrl(url: string): string | undefined {350try {351const parsed = new URL(url);352const match = parsed.pathname.match(/^\/([^/]+)\/(?:status|article)\//);353if (!match) {354return undefined;355}356if (match[1] === "i") {357return undefined;358}359return match[1];360} catch {361return undefined;362}363}364365export function toXTweet(tweet: JsonObject, fallbackUrl: string): XTweet {366const legacy = getLegacy(tweet);367const user = getUser(tweet);368const fallbackScreenName = extractScreenNameFromUrl(fallbackUrl);369const screenName = user.screenName ?? fallbackScreenName;370return {371id: typeof tweet.rest_id === "string" ? tweet.rest_id : "",372author: screenName,373authorName: user.name,374text: getTweetText(tweet),375likes: typeof legacy.favorite_count === "number" ? legacy.favorite_count : 0,376retweets: typeof legacy.retweet_count === "number" ? legacy.retweet_count : 0,377replies: typeof legacy.reply_count === "number" ? legacy.reply_count : 0,378createdAt: typeof legacy.created_at === "string" ? legacy.created_at : undefined,379inReplyTo: typeof legacy.in_reply_to_status_id_str === "string" ? legacy.in_reply_to_status_id_str : undefined,380url: getTweetUrl(tweet, fallbackUrl),381media: getTweetMedia(tweet),382quotedTweet: getQuotedTweet(tweet, fallbackUrl),383};384}385386export function normalizeTitle(text: string, fallback: string): string {387const firstLine = text.split("\n")[0]?.trim();388if (!firstLine) {389return fallback;390}391return firstLine.slice(0, 120);392}393394export function formatTweetAuthor(tweet: XTweet): string | undefined {395if (tweet.author && tweet.authorName) {396return `${tweet.authorName} (@${tweet.author})`;397}398if (tweet.author) {399return `@${tweet.author}`;400}401return tweet.authorName;402}403404export function getTweetAuthorMetadata(tweet: XTweet): Record<string, unknown> {405return {406authorName: tweet.authorName,407authorUsername: tweet.author,408authorUrl: tweet.author ? `https://x.com/${tweet.author}` : undefined,409};410}411412export function formatMediaList(media: XMedia[]): string[] {413return media.map((item) => {414if (item.type === "photo") {415return `photo: ${item.url}`;416}417return `${item.type}: ${item.url}`;418});419}420421export function filterXGraphQlEntries(entries: NetworkEntry[]): NetworkEntry[] {422return entries.filter((entry) => entry.url.includes("/graphql/"));423}424