feat(route): instagram from cookie (#11952)

* feat(route): instagram 2

* docs: add docs

* fix: get tabs

* docs: fix tag
This commit is contained in:
Tony
2023-02-25 15:12:06 +08:00
committed by GitHub
parent a7cbbf559b
commit 68e972464c
11 changed files with 259 additions and 71 deletions
+1
View File
@@ -185,6 +185,7 @@ const calculateValue = () => {
username: envs.IG_USERNAME,
password: envs.IG_PASSWORD,
proxy: envs.IG_PROXY,
cookie: envs.IG_COOKIE,
},
iwara: {
cookie: envs.IWARA_COOKIE,
+59
View File
@@ -0,0 +1,59 @@
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const renderItems = (items) =>
items.map((item) => {
const { product_type } = item; // carousel_container, feed, clips, igtv
// Content
const summary = item.caption?.text ?? '';
let description = '';
switch (product_type) {
case 'carousel_container': {
const images = item.carousel_media.map((i) => i.image_versions2.candidates[0]);
description = art(path.join(__dirname, 'templates/images.art'), {
summary,
images,
});
break;
}
case 'clips':
case 'igtv':
description = art(path.join(__dirname, 'templates/video.art'), {
summary,
image: item.image_versions2.candidates[0].url,
video: item.video_versions[0],
});
break;
case 'feed': {
const images = [item.image_versions2.candidates[0]];
description = art(path.join(__dirname, 'templates/images.art'), {
summary,
images,
});
break;
}
default:
throw Error(`Instagram: Unhandled feed type: ${product_type}`);
}
// Metadata
const url = `https://www.instagram.com/p/${item.code}/`;
const pubDate = parseDate(item.taken_at, 'X');
const title = summary.split('\n')[0];
return {
title,
id: item.pk,
pubDate,
author: item.user.username,
link: url,
summary,
description,
};
});
module.exports = {
renderItems,
};
+1
View File
@@ -1,3 +1,4 @@
module.exports = {
'/:category/:key': ['oppilate', 'DIYgod'],
'/2/:category/:key': ['TonyRL'],
};
+5 -58
View File
@@ -1,9 +1,7 @@
const { ig, login } = require('./utils');
const logger = require('@/utils/logger');
const config = require('@/config').value;
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const { renderItems } = require('../common-utils');
// loadContent pulls the desired user/tag/etc
async function loadContent(category, nameOrId, tryGet) {
@@ -24,7 +22,8 @@ async function loadContent(category, nameOrId, tryGet) {
}
feedDescription = userInfo.biography;
feedLogo = userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
// exists in web api ?? exist in private api ?? exist in both
feedLogo = userInfo.profile_pic_url_hd ?? userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
const fullName = userInfo.full_name;
feedTitle = `${fullName} (@${username}) - Instagram`;
feedLink = `https://www.instagram.com/${username}`;
@@ -41,9 +40,8 @@ async function loadContent(category, nameOrId, tryGet) {
itemsRaw = await tryGet(`instagram:tags:${tag}`, () => ig.feed.tags(tag, 'recent').items(), config.cache.routeExpire, false);
break;
}
default: {
default:
break;
}
}
return {
@@ -83,62 +81,11 @@ module.exports = async (ctx) => {
throw e;
}
const items = data.itemsRaw.map((item) => {
const { product_type } = item; // carousel_container, feed, clips, igtv
// Content
const summary = item.caption?.text ?? '';
let description = '';
switch (product_type) {
case 'carousel_container': {
const images = item.carousel_media.map((i) => i.image_versions2.candidates[0]);
description = art(path.join(__dirname, '../templates/images.art'), {
summary,
images,
});
break;
}
case 'clips':
case 'igtv':
description = art(path.join(__dirname, '../templates/video.art'), {
summary,
image: item.image_versions2.candidates[0].url,
video: item.video_versions[0],
});
break;
case 'feed': {
const images = [item.image_versions2.candidates[0]];
description = art(path.join(__dirname, '../templates/images.art'), {
summary,
images,
});
break;
}
default:
throw Error(`Instagram: Unhandled feed type: ${product_type}`);
}
// Metadata
const url = `https://www.instagram.com/p/${item.code}/`;
const pubDate = parseDate(item.taken_at, 'X');
const title = summary.split('\n')[0];
return {
title,
id: item.pk,
pubDate,
author: item.user.username,
link: url,
summary,
description,
};
});
ctx.state.data = {
title: data.feedTitle,
link: data.feedLink,
description: data.feedDescription,
item: items,
item: renderItems(data.itemsRaw),
icon: 'https://www.instagram.com/static/images/ico/xxhdpi_launcher.png/99cf3909d459.png',
logo: data.feedLogo,
image: data.feedLogo,
+1
View File
@@ -1,3 +1,4 @@
module.exports = (router) => {
router.get('/:category/:key', require('./private-api/index'));
router.get('/2/:category/:key', require('./web-api/index'));
};
+70
View File
@@ -0,0 +1,70 @@
const { CookieJar } = require('tough-cookie');
const config = require('@/config').value;
const { renderItems } = require('../common-utils');
const { baseUrl, COOKIE_URL, getUserInfo, getUserFeedItems, getTagsFeedItems } = require('./utils');
module.exports = async (ctx) => {
if (!config.instagram || !config.instagram.cookie) {
throw Error('Instagram RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}
const availableCategories = ['user', 'tags'];
const { category, key } = ctx.params;
const { cookie } = config.instagram;
if (!availableCategories.includes(category)) {
throw Error('Such feed is not supported.');
}
let cookieJar = await ctx.cache.get('instagram:cookieJar');
const cacheMiss = !cookieJar;
if (cacheMiss) {
cookieJar = new CookieJar();
for await (const c of cookie.split('; ')) {
await cookieJar.setCookie(c, COOKIE_URL);
}
} else {
cookieJar = CookieJar.fromJSON(cookieJar);
}
let feedTitle, feedLink, feedDescription, feedLogo;
let items;
switch (category) {
case 'user': {
const userInfo = await getUserInfo(key, cookieJar, ctx.cache);
// User feed metadata
const { biography, full_name, id, username } = userInfo;
feedTitle = `${full_name} (@${username}) - Instagram`;
feedDescription = biography;
// exists in web api ?? exist in private api ?? exist in both
feedLogo = userInfo.profile_pic_url_hd ?? userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
feedLink = `${baseUrl}/${username}`;
items = await getUserFeedItems(id, username, cookieJar, ctx.cache.tryGet);
break;
}
case 'tags': {
const tag = key;
feedTitle = `#${tag} - Instagram`;
feedLink = `${baseUrl}/explore/tags/${tag}`;
items = await getTagsFeedItems(tag, 'recent', cookieJar, ctx.cache.tryGet);
break;
}
default:
break;
}
await ctx.cache.set('instagram:cookieJar', cookieJar.toJSON(), 31536000);
ctx.state.data = {
title: feedTitle,
link: feedLink,
description: feedDescription,
item: renderItems(items),
icon: `${baseUrl}/static/images/ico/xxhdpi_launcher.png/99cf3909d459.png`,
logo: feedLogo,
image: feedLogo,
allowEmpty: true,
};
};
+99
View File
@@ -0,0 +1,99 @@
const got = require('@/utils/got');
const config = require('@/config').value;
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);
return cookieString.match(/csrftoken=([^;]+)/)?.[1];
};
const getHeaders = async (cookieJar) => ({
'X-ASBD-ID': 198387,
'X-CSRFToken': await getCSRFTokenFromJar(cookieJar),
'X-IG-App-ID': 936619743392459,
'X-IG-WWW-Claim': igWwwClaim,
});
const getUserInfo = async (username, cookieJar, cache) => {
let webProfileInfo;
let id = await cache.get(`instagram:getIdByUsername:${username}`);
let userInfoCache = await cache.get(`instagram:userInfo:${id}`);
if (!userInfoCache) {
const response = await got(`${baseUrl}/api/v1/users/web_profile_info/`, {
cookieJar,
headers: await getHeaders(cookieJar),
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;
await cache.set(`instagram:getIdByUsername:${username}`, id, 31536000); // 1 year since it will never change
await cache.set(`instagram:userInfo:${id}`, webProfileInfo);
}
userInfoCache = typeof userInfoCache === 'string' ? JSON.parse(userInfoCache) : userInfoCache;
return userInfoCache || webProfileInfo;
};
const getUserFeedItems = (id, username, cookieJar, tryGet) =>
tryGet(
`instagram:feed:${id}`,
async () => {
const response = await got(`${baseUrl}/api/v1/feed/user/${username}/username/`, {
cookieJar,
headers: await getHeaders(cookieJar),
searchParams: {
count: 30,
},
});
// 401 Unauthorized if cookie does not match with IP
igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim;
return response.data.items;
},
config.cache.routeExpire,
false
);
const getTagsFeedItems = (tag, tab, cookieJar, tryGet) =>
tryGet(
`instagram:tags:${tag}`,
async () => {
const response = await got(`${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)),
},
searchParams: {
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
);
module.exports = {
baseUrl,
COOKIE_URL,
getUserInfo,
getUserFeedItems,
getTagsFeedItems,
};