Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Convert X (Twitter) tweets, threads, and articles to Markdown with YAML front matter via reverse-engineered API.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/tweet-article.ts
1import { fetchXArticle } from "./graphql.js";2import type { ArticleEntity } from "./types.js";34function coerceArticleEntity(value: unknown): ArticleEntity | null {5if (!value || typeof value !== "object") return null;6const candidate = value as ArticleEntity;7if (8typeof candidate.title === "string" ||9typeof candidate.plain_text === "string" ||10typeof candidate.preview_text === "string" ||11candidate.content_state12) {13return candidate;14}15return null;16}1718function hasArticleContent(article: ArticleEntity): boolean {19const blocks = article.content_state?.blocks;20if (Array.isArray(blocks) && blocks.length > 0) {21return true;22}23if (typeof article.plain_text === "string" && article.plain_text.trim()) {24return true;25}26if (typeof article.preview_text === "string" && article.preview_text.trim()) {27return true;28}29return false;30}3132function parseArticleIdFromUrl(raw: string | undefined): string | null {33if (!raw) return null;34try {35const parsed = new URL(raw);36const match = parsed.pathname.match(/\/(?:i\/)?article\/(\d+)/);37if (match?.[1]) return match[1];38} catch {39return null;40}41return null;42}4344function extractArticleIdFromUrls(urls: any[] | undefined): string | null {45if (!Array.isArray(urls)) return null;46for (const url of urls) {47const candidate =48url?.expanded_url ?? url?.url ?? (url?.display_url ? `https://${url.display_url}` : undefined);49const id = parseArticleIdFromUrl(candidate);50if (id) return id;51}52return null;53}5455export function extractArticleEntityFromTweet(tweet: any): unknown | null {56return (57tweet?.article?.article_results?.result ??58tweet?.article?.result ??59tweet?.legacy?.article?.article_results?.result ??60tweet?.legacy?.article?.result ??61tweet?.article_results?.result ??62null63);64}6566export function extractArticleIdFromTweet(tweet: any): string | null {67const embedded = extractArticleEntityFromTweet(tweet);68const embeddedArticle = embedded as { rest_id?: string } | null;69if (embeddedArticle?.rest_id) {70return embeddedArticle.rest_id;71}7273const noteUrls = tweet?.note_tweet?.note_tweet_results?.result?.entity_set?.urls;74const legacyUrls = tweet?.legacy?.entities?.urls;75return extractArticleIdFromUrls(noteUrls) ?? extractArticleIdFromUrls(legacyUrls);76}7778export async function resolveArticleEntityFromTweet(79tweet: any,80cookieMap: Record<string, string>81): Promise<unknown | null> {82if (!tweet) return null;83const embedded = extractArticleEntityFromTweet(tweet);84const embeddedArticle = coerceArticleEntity(embedded);85if (embeddedArticle && hasArticleContent(embeddedArticle)) {86return embedded;87}8889const articleId = extractArticleIdFromTweet(tweet);90if (!articleId) {91return embedded ?? null;92}9394const fetched = await fetchXArticle(articleId, cookieMap, false);95return fetched ?? embedded ?? null;96}97