const got = require('@/utils/got'); const cheerio = require('cheerio'); const { parseDate } = require('@/utils/parse-date'); const dayjs = require('dayjs'); const profileUrl = (user) => `https://www.threads.net/@${user}`; const threadUrl = (code) => `https://www.threads.net/t/${code}`; const apiUrl = 'https://www.threads.net/api/graphql'; const THREADS_QUERY = 6232751443445612; const REPLIES_QUERY = 6307072669391286; const USER_AGENT = 'Barcelona 289.0.0.77.109 Android'; const load = async (url) => { // user id fetching needs puppeteer const browser = await require('@/utils/puppeteer')(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { request.resourceType() === 'document' ? request.continue() : request.abort(); }); await page.goto(url, { waitUntil: 'domcontentloaded', }); const response = await page.content(); browser.close(); return cheerio.load(response); }; const extractTokens = async (user, ctx) => { const $ = await load(profileUrl(user)); const lsd = $('script:contains("LSD"):first') .html() .match(/"LSD",\[],\{"token":"([a-zA-Z0-9@_-]+)"},/)?.[1]; // This needs puppeteer const userId = $('script:contains("user_id"):first') .html() .match(/"user_id":"(\d+)"/)?.[1]; ctx.state.json = { lsd, userId }; return { lsd, userId }; }; const makeHeader = (user, lsd) => ({ Accept: 'application/json', Host: 'www.threads.net', Origin: 'https://www.threads.net', Referer: `https://www.threads.net/@${user}`, 'User-Agent': USER_AGENT, 'X-FB-LSD': lsd, 'X-IG-App-ID': '238260118697367', }); const hasMedia = (post) => post.image_versions2 || post.carousel_media || post.video_versions; const buildMedia = (post) => { let html = ''; if (!post.carousel_media) { const mainImage = post.image_versions2?.candidates?.[0]; const mainVideo = post.video_versions?.[0]; if (mainImage) { if (!mainVideo) { html += ``; } else { html += `'; } } } else { post.carousel_media.forEach((media) => { const firstImage = media.image_versions2?.candidates[0]; const firstVideo = media.video_versions?.[0]; if (!firstVideo) { html += ``; } else { html += `'; } }); } return html; }; const buildContent = (item, options) => { let title = ''; let description = ''; const quotedPost = item.post.text_post_app_info?.share_info?.quoted_post; const repostedPost = item.post.text_post_app_info?.share_info?.reposted_post; const isReply = item.post.text_post_app_info?.reply_to_author; const embededPost = quotedPost ?? repostedPost; if (options.showAuthorInTitle) { title += `@${item.post.user?.username}: `; } if (options.showAuthorInDesc) { description += '

'; if (options.showAuthorAvatarInDesc) { description += ` `; } description += `@${item.post.user?.username}`; if (embededPost) { description += options.showEmojiForQuotesAndReply ? ' 🔁' : ' quoted'; } else if (isReply) { description += options.showEmojiForQuotesAndReply ? ' â†Šī¸' : ' replied'; } description += ':

'; } if (item.post.caption?.text) { title += item.post.caption?.text; description += `

${item.post.caption?.text}

`; } if (hasMedia(item.post)) { description += `

${buildMedia(item.post)}

`; } if (embededPost) { if (options.showQuotedInTitle) { title += options.showEmojiForQuotesAndReply ? ' 🔁 ' : ' QT: '; title += `@${embededPost.user?.username}: `; title += `"${embededPost.caption?.text}"`; } description += '
'; description += `

${embededPost.caption?.text}

`; if (hasMedia(embededPost)) { description += `

${buildMedia(embededPost)}

`; } description += '— '; if (options.showQuotedAuthorAvatarInDesc) { description += ` `; } description += `@${embededPost.user?.username} — `; description += `${dayjs(embededPost.taken_at, 'X').toString()}`; description += '
'; } return { title, description }; }; module.exports = async (ctx) => { const { user, routeParams } = ctx.params; const { lsd, userId } = await extractTokens(user, ctx); const params = new URLSearchParams(routeParams); ctx.state.json.params = routeParams; const options = { showAuthorInTitle: params.get('showAuthorInTitle') ?? true, showAuthorInDesc: params.get('showAuthorInDesc') ?? true, showAuthorAvatarInDesc: params.get('showAuthorAvatarInDesc') ?? false, showQuotedInTitle: params.get('showQuotedInTitle') ?? true, showQuotedAuthorAvatarInDesc: params.get('showQuotedAuthorAvatarInDesc') ?? false, showEmojiForQuotesAndReply: params.get('showEmojiForQuotesAndReply') ?? true, replies: params.get('replies') ?? false, }; const headers = makeHeader(user, lsd); const resp = await got.post(apiUrl, { headers, form: { lsd, variables: JSON.stringify({ userID: userId }), doc_id: options.replies ? REPLIES_QUERY : THREADS_QUERY, }, }); ctx.state.json.request = { headers: resp.request.options.headers, body: resp.request.options.body, }; const responseBody = resp.data; const threads = responseBody?.data?.mediaData?.threads || []; const items = threads.flatMap((thread) => thread.thread_items .filter((item) => user === item.post.user?.username) .map((item) => { const { title, description } = buildContent(item, options); return { author: user, title, description, pubDate: parseDate(item.post.taken_at, 'X'), link: threadUrl(item.post.code), }; }) ); ctx.state.json.items = items; ctx.state.data = { title: user, link: profileUrl(user), description: user, item: items, }; };