Skip to content

Commit

Permalink
feat(route/instagram): authless tags (#13944)
Browse files Browse the repository at this point in the history
* feat(route/instagram): authless tags

* fix: wwwClaimV2 condition
  • Loading branch information
TonyRL authored Dec 3, 2023
1 parent c06353f commit d7449b3
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 38 deletions.
11 changes: 7 additions & 4 deletions lib/v2/instagram/common-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions lib/v2/instagram/templates/images.art
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
<img src="{{ i.url }}"
{{ if i.height }} height="{{ i.height }}" {{ /if }}
{{ if i.width }} width="{{ i.width }}" {{ /if }}
{{ if i.alt }} alt="{{ i.alt }}" {{ /if }}
>
{{ /each }}
29 changes: 20 additions & 9 deletions lib/v2/instagram/web-api/index.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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;
Expand All @@ -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];
}
Expand All @@ -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:
Expand Down
103 changes: 78 additions & 25 deletions lib/v2/instagram/web-api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}`);
Expand All @@ -29,15 +47,17 @@ 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,
},
});
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;
Expand All @@ -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;
},
Expand All @@ -91,15 +118,36 @@ 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));
},
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'), {
Expand All @@ -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);
Expand All @@ -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}/`,
Expand All @@ -163,8 +214,10 @@ const renderGuestItems = (items) => {
module.exports = {
baseUrl,
COOKIE_URL,
checkLogin,
getUserInfo,
getUserFeedItems,
getTagsFeedItems,
getLoggedOutTagsFeedItems,
renderGuestItems,
};

0 comments on commit d7449b3

Please sign in to comment.