From d7449b3a77abc88c20cebb60feb4388d3a6e493f Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 3 Dec 2023 16:15:57 +0000 Subject: [PATCH] feat(route/instagram): authless tags (#13944) * feat(route/instagram): authless tags * fix: wwwClaimV2 condition --- lib/v2/instagram/common-utils.js | 11 ++- lib/v2/instagram/templates/images.art | 1 + lib/v2/instagram/web-api/index.js | 29 +++++--- lib/v2/instagram/web-api/utils.js | 103 +++++++++++++++++++------- 4 files changed, 106 insertions(+), 38 deletions(-) diff --git a/lib/v2/instagram/common-utils.js b/lib/v2/instagram/common-utils.js index 8ab6c32e9c..613662c87d 100644 --- a/lib/v2/instagram/common-utils.js +++ b/lib/v2/instagram/common-utils.js @@ -11,7 +11,10 @@ const renderItems = (items) => let description = ''; switch (product_type) { case 'carousel_container': { - const images = item.carousel_media.map((i) => i.image_versions2.candidates[0]); + const images = item.carousel_media.map((i) => ({ + ...i.image_versions2.candidates.sort((a, b) => b.width - a.width)[0], + alt: item.accessibility_caption, + })); description = art(path.join(__dirname, 'templates/images.art'), { summary, images, @@ -22,12 +25,12 @@ const renderItems = (items) => case 'igtv': description = art(path.join(__dirname, 'templates/video.art'), { summary, - image: item.image_versions2.candidates[0].url, + image: item.image_versions2.candidates.sort((a, b) => b.width - a.width)[0], video: item.video_versions[0], }); break; case 'feed': { - const images = [item.image_versions2.candidates[0]]; + const images = [{ ...item.image_versions2.candidates.sort((a, b) => b.width - a.width)[0], alt: item.accessibility_caption }]; description = art(path.join(__dirname, 'templates/images.art'), { summary, images, @@ -40,7 +43,7 @@ const renderItems = (items) => // Metadata const url = `https://www.instagram.com/p/${item.code}/`; - const pubDate = parseDate(item.taken_at, 'X'); + const pubDate = parseDate(item.caption.created_at_utc || item.taken_at, 'X'); const title = summary.split('\n')[0]; return { diff --git a/lib/v2/instagram/templates/images.art b/lib/v2/instagram/templates/images.art index 3af7e6ea5e..ca6cc69ec0 100644 --- a/lib/v2/instagram/templates/images.art +++ b/lib/v2/instagram/templates/images.art @@ -7,5 +7,6 @@ {{ i.alt }} {{ /each }} diff --git a/lib/v2/instagram/web-api/index.js b/lib/v2/instagram/web-api/index.js index 6482f21426..ef06832863 100644 --- a/lib/v2/instagram/web-api/index.js +++ b/lib/v2/instagram/web-api/index.js @@ -1,7 +1,7 @@ const { CookieJar } = require('tough-cookie'); const config = require('@/config').value; const { renderItems } = require('../common-utils'); -const { baseUrl, COOKIE_URL, getUserInfo, getUserFeedItems, getTagsFeedItems, renderGuestItems } = require('./utils'); +const { baseUrl, COOKIE_URL, checkLogin, getUserInfo, getUserFeedItems, getTagsFeedItems, getLoggedOutTagsFeedItems, renderGuestItems } = require('./utils'); module.exports = async (ctx) => { // if (!config.instagram || !config.instagram.cookie) { @@ -15,19 +15,24 @@ module.exports = async (ctx) => { } let cookieJar = await ctx.cache.get('instagram:cookieJar'); + const wwwClaimV2 = await ctx.cache.get('instagram:wwwClaimV2'); const cacheMiss = !cookieJar; - if (cookie) { - if (cacheMiss) { - cookieJar = new CookieJar(); + if (cacheMiss) { + cookieJar = new CookieJar(); + if (cookie) { for await (const c of cookie.split('; ')) { await cookieJar.setCookie(c, COOKIE_URL); } - } else { - cookieJar = CookieJar.fromJSON(cookieJar); } } else { - cookieJar = new CookieJar(); + cookieJar = CookieJar.fromJSON(cookieJar); + } + + if (!wwwClaimV2 && cookie) { + if (!(await checkLogin(cookieJar, ctx.cache))) { + throw Error('Invalid cookie'); + } } let feedTitle, feedLink, feedDescription, feedLogo; @@ -45,7 +50,7 @@ module.exports = async (ctx) => { feedLink = `${baseUrl}/${username}`; if (cookie) { - items = await getUserFeedItems(id, username, cookieJar, ctx.cache.tryGet); + items = await getUserFeedItems(id, username, cookieJar, ctx.cache); } else { items = [...userInfo.edge_felix_video_timeline.edges, ...userInfo.edge_owner_to_timeline_media.edges]; } @@ -58,7 +63,13 @@ module.exports = async (ctx) => { feedTitle = `#${tag} - Instagram`; feedLink = `${baseUrl}/explore/tags/${tag}`; - items = await getTagsFeedItems(tag, 'recent', cookieJar, ctx.cache.tryGet); + if (cookie) { + items = await getTagsFeedItems(tag, 'recent', cookieJar, ctx.cache.tryGet); + } else { + const hashTag = await getLoggedOutTagsFeedItems(tag, cookieJar); + items = [...hashTag.edge_hashtag_to_media.edges, ...hashTag.edge_hashtag_to_top_posts.edges]; + } + break; } default: diff --git a/lib/v2/instagram/web-api/utils.js b/lib/v2/instagram/web-api/utils.js index a21b93db0c..9874ac36ea 100644 --- a/lib/v2/instagram/web-api/utils.js +++ b/lib/v2/instagram/web-api/utils.js @@ -6,7 +6,6 @@ const path = require('path'); const baseUrl = 'https://www.instagram.com'; const COOKIE_URL = 'https://instagram.com'; -let igWwwClaim; const getCSRFTokenFromJar = async (cookieJar) => { const cookieString = await cookieJar.getCookieString(COOKIE_URL); @@ -17,9 +16,28 @@ const getHeaders = async (cookieJar) => ({ 'X-ASBD-ID': 198387, 'X-CSRFToken': await getCSRFTokenFromJar(cookieJar), 'X-IG-App-ID': 936619743392459, - 'X-IG-WWW-Claim': igWwwClaim, + 'X-IG-WWW-Claim': undefined, }); +const checkLogin = async (cookieJar, cache) => { + const response = await got.post(`${baseUrl}/api/v1/web/fxcal/ig_sso_users/`, { + // cookieJar, + headers: { + cookie: await cookieJar.getCookieString(COOKIE_URL), + ...(await getHeaders(cookieJar)), + 'X-IG-WWW-Claim': '0', + }, + }); + + const wwwClaimV2 = response.headers['x-ig-set-www-claim']; + + if (wwwClaimV2) { + cache.set('instagram:wwwClaimV2', wwwClaimV2); + } + + return Boolean(response.data.status === 'ok'); +}; + const getUserInfo = async (username, cookieJar, cache) => { let webProfileInfo; let id = await cache.get(`instagram:getIdByUsername:${username}`); @@ -29,7 +47,10 @@ const getUserInfo = async (username, cookieJar, cache) => { try { const response = await got(`${baseUrl}/api/v1/users/web_profile_info/`, { cookieJar, - headers: await getHeaders(cookieJar), + headers: { + ...(await getHeaders(cookieJar)), + 'X-IG-WWW-Claim': await cache.get('instagram:wwwClaimV2'), + }, searchParams: { username, }, @@ -37,7 +58,6 @@ const getUserInfo = async (username, cookieJar, cache) => { if (response.url.includes('/accounts/login/')) { throw Error('Invalid cookie'); } - igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim; webProfileInfo = response.data.data.user; id = webProfileInfo.id; @@ -57,19 +77,26 @@ const getUserInfo = async (username, cookieJar, cache) => { return userInfoCache || webProfileInfo; }; -const getUserFeedItems = (id, username, cookieJar, tryGet) => - tryGet( +const getUserFeedItems = (id, username, cookieJar, cache) => + cache.tryGet( `instagram:feed:${id}`, async () => { const response = await got(`${baseUrl}/api/v1/feed/user/${username}/username/`, { - cookieJar, - headers: await getHeaders(cookieJar), + // cookieJar, + headers: { + cookie: await cookieJar.getCookieString(COOKIE_URL), + ...(await getHeaders(cookieJar)), + // 401 Unauthorized if cookie does not match with IP + 'X-IG-WWW-Claim': await cache.get('instagram:wwwClaimV2'), + }, searchParams: { count: 30, }, }); - // 401 Unauthorized if cookie does not match with IP - igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim; + if (response.url.includes('/accounts/login/')) { + throw Error(`Invalid cookie. + Please also check if your account is being blocked by Instagram.`); + } return response.data.items; }, @@ -91,8 +118,6 @@ const getTagsFeedItems = (tag, tab, cookieJar, tryGet) => tag_name: tag, }, }); - // Looks like cookie IP check is not applied to tags - igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim; return response.data.data[tab].sections.flatMap((section) => section.layout_content.medias.map((media) => media.media)); }, @@ -100,6 +125,29 @@ const getTagsFeedItems = (tag, tab, cookieJar, tryGet) => false ); +const getLoggedOutTagsFeedItems = async (tag, cookieJar) => { + const response = await got(`${baseUrl}/api/v1/tags/logged_out_web_info/`, { + // cookieJar, cookieJar is behaving weirdly here, so we use cookie header instead + headers: { + cookie: await cookieJar.getCookieString(COOKIE_URL), + ...(await getHeaders(cookieJar)), + 'X-IG-WWW-Claim': '0', + }, + searchParams: { + tag_name: tag, + }, + }); + + const cookies = response.headers['set-cookie']; + if (cookies) { + for await (const cookie of cookies) { + await cookieJar.setCookie(cookie, COOKIE_URL); + } + } + + return response.data.data.hashtag; +}; + const renderGuestItems = (items) => { const renderVideo = (node, summary) => art(path.join(__dirname, '../templates/video.art'), { @@ -125,19 +173,21 @@ const renderGuestItems = (items) => { switch (type) { // carousel, can include GraphVideo and GraphImage case 'GraphSidecar': - description = node.edge_sidecar_to_children.edges - .map(({ node }, i) => { - const _type = node.__typename; - switch (_type) { - case 'GraphVideo': - return renderVideo(node, i === 0 ? summary : ''); - case 'GraphImage': - return renderImages(node, i === 0 ? summary : ''); - default: - throw Error(`Instagram: Unhandled carousel type: ${_type}`); - } - }) - .join(''); + description = node.edge_sidecar_to_children + ? node.edge_sidecar_to_children.edges + .map(({ node }, i) => { + const _type = node.__typename; + switch (_type) { + case 'GraphVideo': + return renderVideo(node, i === 0 ? summary : ''); + case 'GraphImage': + return renderImages(node, i === 0 ? summary : ''); + default: + throw Error(`Instagram: Unhandled carousel type: ${_type}`); + } + }) + .join('') + : renderImages(node, summary); break; case 'GraphVideo': description = renderVideo(node, summary); @@ -151,6 +201,7 @@ const renderGuestItems = (items) => { return { title: summary.split('\n')[0], + id: node.id, pubDate: parseDate(node.taken_at_timestamp, 'X'), author: node.owner.username, link: `${baseUrl}/p/${node.shortcode}/`, @@ -163,8 +214,10 @@ const renderGuestItems = (items) => { module.exports = { baseUrl, COOKIE_URL, + checkLogin, getUserInfo, getUserFeedItems, getTagsFeedItems, + getLoggedOutTagsFeedItems, renderGuestItems, };