diff --git a/lib/routes/instagram/web-api/index.ts b/lib/routes/instagram/web-api/index.ts index f390861868..cf80edef45 100644 --- a/lib/routes/instagram/web-api/index.ts +++ b/lib/routes/instagram/web-api/index.ts @@ -3,7 +3,7 @@ import cache from '@/utils/cache'; import { CookieJar } from 'tough-cookie'; import { config } from '@/config'; import { renderItems } from '../common-utils'; -import { baseUrl, COOKIE_URL, checkLogin, getUserInfo, getUserFeedItems, getTagsFeedItems, getLoggedOutTagsFeedItems, renderGuestItems } from './utils'; +import { baseUrl, COOKIE_URL, checkLogin, getUserInfo, getUserFeedItems, getTagsFeed, renderGuestItems } from './utils'; import InvalidParameterError from '@/errors/types/invalid-parameter'; import ConfigNotFoundError from '@/errors/types/config-not-found'; @@ -35,7 +35,7 @@ You may need to setup cookie for a less restrictive rate limit and private profi async function handler(ctx) { // if (!config.instagram || !config.instagram.cookie) { - // throw Error('Instagram RSS is disabled due to the lack of relevant config'); + // throw new ConfigNotFoundError('Instagram RSS is disabled due to the lack of relevant config'); // } const availableCategories = ['user', 'tags']; const { category, key } = ctx.req.param(); @@ -45,7 +45,7 @@ async function handler(ctx) { } let cookieJar = await cache.get('instagram:cookieJar'); - const wwwClaimV2 = await cache.get('instagram:wwwClaimV2'); + // const wwwClaimV2 = await cache.get('instagram:wwwClaimV2'); const cacheMiss = !cookieJar; if (cacheMiss) { @@ -59,7 +59,7 @@ async function handler(ctx) { cookieJar = CookieJar.fromJSON(cookieJar); } - if (!wwwClaimV2 && cookie && !(await checkLogin(cookieJar, cache))) { + if (/* !wwwClaimV2 &&*/ cookie && !(await checkLogin(cookieJar))) { throw new ConfigNotFoundError('Invalid cookie'); } @@ -67,7 +67,7 @@ async function handler(ctx) { let items; switch (category) { case 'user': { - const userInfo = await getUserInfo(key, cookieJar, cache); + const userInfo = await getUserInfo(key, cookieJar); // User feed metadata const biography = userInfo.biography; @@ -80,22 +80,26 @@ async function handler(ctx) { feedLogo = userInfo.profile_pic_url_hd ?? userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url; feedLink = `${baseUrl}/${username}`; - items = cookie ? await getUserFeedItems(id, username, cookieJar, cache) : [...userInfo.edge_felix_video_timeline.edges, ...userInfo.edge_owner_to_timeline_media.edges]; + items = cookie ? await getUserFeedItems(id, username, cookieJar) : [...userInfo.edge_felix_video_timeline.edges, ...userInfo.edge_owner_to_timeline_media.edges]; break; } case 'tags': { + if (!config.instagram || !config.instagram.cookie) { + throw new ConfigNotFoundError('Instagram RSS is disabled due to the lack of relevant config'); + } const tag = key; feedTitle = `#${tag} - Instagram`; - feedLink = `${baseUrl}/explore/tags/${tag}`; + feedLink = `${baseUrl}/explore/search/keyword/?q=%23${tag}`; - if (cookie) { - items = await getTagsFeedItems(tag, 'recent', cookieJar, cache.tryGet); - } else { - const hashTag = await getLoggedOutTagsFeedItems(tag, cookieJar); - items = [...hashTag.edge_hashtag_to_media.edges, ...hashTag.edge_hashtag_to_top_posts.edges]; - } + const feedData = await getTagsFeed(tag, cookieJar); + + feedLogo = feedData.profile_pic_url; + items = feedData.top.sections.flatMap((section) => + // either media or clips + (section.feed_type === 'media' ? section.layout_content.medias : [...section.layout_content.one_by_two_item.clips.items, ...section.layout_content.fill_items]).map((item) => item.media) + ); break; } diff --git a/lib/routes/instagram/web-api/utils.ts b/lib/routes/instagram/web-api/utils.ts index cae877ac47..062025ff7d 100644 --- a/lib/routes/instagram/web-api/utils.ts +++ b/lib/routes/instagram/web-api/utils.ts @@ -1,4 +1,5 @@ -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; import { art } from '@/utils/render'; @@ -6,7 +7,7 @@ import path from 'node:path'; import ConfigNotFoundError from '@/errors/types/config-not-found'; const baseUrl = 'https://www.instagram.com'; -const COOKIE_URL = 'https://instagram.com'; +const COOKIE_URL = baseUrl; const getCSRFTokenFromJar = async (cookieJar) => { const cookieString = await cookieJar.getCookieString(COOKIE_URL); @@ -14,45 +15,52 @@ const getCSRFTokenFromJar = async (cookieJar) => { }; const getHeaders = async (cookieJar) => ({ - 'X-ASBD-ID': 198387, - 'X-CSRFToken': await getCSRFTokenFromJar(cookieJar), - 'X-IG-App-ID': 936_619_743_392_459, - 'X-IG-WWW-Claim': undefined, + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'x-asbd-id': 359341, + 'x-csrftoken': await getCSRFTokenFromJar(cookieJar), + 'x-ig-app-id': 936_619_743_392_459, + 'x-ig-www-claim': '0', }); -const checkLogin = async (cookieJar, cache) => { - const response = await got.post(`${baseUrl}/api/v1/web/fxcal/ig_sso_users/`, { +const checkLogin = async (cookieJar) => { + const response = await ofetch(`${baseUrl}/api/v1/web/fxcal/ig_sso_users/`, { // cookieJar, headers: { - cookie: await cookieJar.getCookieString(COOKIE_URL), - ...(await getHeaders(cookieJar)), - 'X-IG-WWW-Claim': '0', + 'content-type': 'application/x-www-form-urlencoded', + cookie: (await cookieJar.getCookieString(COOKIE_URL)) as string, + ...((await getHeaders(cookieJar)) as unknown as Record), + // 'X-IG-WWW-Claim': '0', }, + method: 'POST', }); - const wwwClaimV2 = response.headers['x-ig-set-www-claim']; + // const wwwClaimV2 = response.headers['x-ig-set-www-claim']; - if (wwwClaimV2) { - cache.set('instagram:wwwClaimV2', wwwClaimV2); - } + // if (wwwClaimV2) { + // cache.set('instagram:wwwClaimV2', wwwClaimV2); + // } - return Boolean(response.data.status === 'ok'); + return Boolean(response.status === 'ok'); }; -const getUserInfo = async (username, cookieJar, cache) => { +const getUserInfo = async (username, cookieJar) => { let webProfileInfo; let id = await cache.get(`instagram:getIdByUsername:${username}`); let userInfoCache = await cache.get(`instagram:userInfo:${id}`); + userInfoCache = userInfoCache && typeof userInfoCache === 'string' ? JSON.parse(userInfoCache) : userInfoCache; if (!userInfoCache) { try { - const response = await got(`${baseUrl}/api/v1/users/web_profile_info/`, { - cookieJar, + const response = await ofetch.raw(`${baseUrl}/api/v1/users/web_profile_info/`, { + // cookieJar, headers: { - ...(await getHeaders(cookieJar)), - 'X-IG-WWW-Claim': (await cache.get('instagram:wwwClaimV2')) ?? undefined, + cookie: (await cookieJar.getCookieString(COOKIE_URL)) as string, + ...((await getHeaders(cookieJar)) as unknown as Record), + // 'X-IG-WWW-Claim': (await cache.get('instagram:wwwClaimV2')) ?? undefined, }, - searchParams: { + query: { username, }, }); @@ -60,7 +68,7 @@ const getUserInfo = async (username, cookieJar, cache) => { throw new ConfigNotFoundError('Invalid cookie'); } - webProfileInfo = response.data.data.user; + webProfileInfo = response._data.data.user; id = webProfileInfo.id; await cache.set(`instagram:getIdByUsername:${username}`, id, 31_536_000); // 1 year since it will never change @@ -73,24 +81,22 @@ const getUserInfo = async (username, cookieJar, cache) => { } } - userInfoCache = typeof userInfoCache === 'string' ? JSON.parse(userInfoCache) : userInfoCache; - return userInfoCache || webProfileInfo; }; -const getUserFeedItems = (id, username, cookieJar, cache) => +const getUserFeedItems = (id, username, cookieJar) => cache.tryGet( `instagram:feed:${id}`, async () => { - const response = await got(`${baseUrl}/api/v1/feed/user/${username}/username/`, { + const response = await ofetch.raw(`${baseUrl}/api/v1/feed/user/${username}/username/`, { // cookieJar, headers: { - cookie: await cookieJar.getCookieString(COOKIE_URL), - ...(await getHeaders(cookieJar)), + cookie: (await cookieJar.getCookieString(COOKIE_URL)) as string, + ...((await getHeaders(cookieJar)) as unknown as Record), // 401 Unauthorized if cookie does not match with IP - 'X-IG-WWW-Claim': await cache.get('instagram:wwwClaimV2'), + // 'X-IG-WWW-Claim': await cache.get('instagram:wwwClaimV2'), }, - searchParams: { + query: { count: 30, }, }); @@ -99,56 +105,33 @@ const getUserFeedItems = (id, username, cookieJar, cache) => Please also check if your account is being blocked by Instagram.`); } - return response.data.items; + return response._data.items; }, config.cache.routeExpire, false ); -const getTagsFeedItems = (tag, tab, cookieJar, tryGet) => - tryGet( +const getTagsFeed = (tag, cookieJar) => + cache.tryGet( `instagram:tags:${tag}`, async () => { - const response = await got(`${baseUrl}/api/v1/tags/web_info/`, { + const response = await ofetch(`${baseUrl}/api/v1/tags/web_info/`, { // cookieJar, cookieJar is behaving weirdly here, so we use cookie header instead headers: { - cookie: await cookieJar.getCookieString(COOKIE_URL), - ...(await getHeaders(cookieJar)), + cookie: (await cookieJar.getCookieString(COOKIE_URL)) as string, + ...((await getHeaders(cookieJar)) as unknown as Record), }, - searchParams: { + query: { tag_name: tag, }, }); - return response.data.data[tab].sections.flatMap((section) => section.layout_content.medias.map((media) => media.media)); + return response.data; }, config.cache.routeExpire, 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'), { @@ -212,4 +195,4 @@ const renderGuestItems = (items) => { }); }; -export { baseUrl, COOKIE_URL, checkLogin, getUserInfo, getUserFeedItems, getTagsFeedItems, getLoggedOutTagsFeedItems, renderGuestItems }; +export { baseUrl, COOKIE_URL, checkLogin, getUserInfo, getUserFeedItems, getTagsFeed, renderGuestItems };