feat(route): authless instagram (#13920)

* feat(route): authless instagram

* docs: prefer instagram gen2 route
This commit is contained in:
Tony 2023-11-29 20:08:27 +00:00 committed by GitHub
parent 52fbf7cdb2
commit 8f236d2759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 49 deletions

View File

@ -1,5 +1,11 @@
{{@ summary.replace(/\n/g, '<br>') }}
{{ if summary }}
{{@ summary.replace(/\n/g, '<br>') }}
<br>
{{ /if }}
{{ each images i }}
<img src="{{ i.url }}" height="{{ i.height }}" width="{{ i.width }}">
<img src="{{ i.url }}"
{{ if i.height }} height="{{ i.height }}" {{ /if }}
{{ if i.width }} width="{{ i.width }}" {{ /if }}
>
{{ /each }}

View File

@ -1,4 +1,7 @@
{{@ summary.replace(/\n/g, '<br>') }}
{{ if summary }}
{{@ summary.replace(/\n/g, '<br>') }}
<br>
{{ /if }}
<video controls preload="none" poster="{{ image }}" width="{{ video.width }}">
<source src="{{ video.url }}" type="video/mp4">

View File

@ -1,12 +1,12 @@
const { CookieJar } = require('tough-cookie');
const config = require('@/config').value;
const { renderItems } = require('../common-utils');
const { baseUrl, COOKIE_URL, getUserInfo, getUserFeedItems, getTagsFeedItems } = require('./utils');
const { baseUrl, COOKIE_URL, getUserInfo, getUserFeedItems, getTagsFeedItems, renderGuestItems } = 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>');
}
// 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;
@ -16,13 +16,18 @@ module.exports = async (ctx) => {
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);
if (cookie) {
if (cacheMiss) {
cookieJar = new CookieJar();
for await (const c of cookie.split('; ')) {
await cookieJar.setCookie(c, COOKIE_URL);
}
} else {
cookieJar = CookieJar.fromJSON(cookieJar);
}
} else {
cookieJar = CookieJar.fromJSON(cookieJar);
cookieJar = new CookieJar();
}
let feedTitle, feedLink, feedDescription, feedLogo;
@ -39,7 +44,12 @@ module.exports = async (ctx) => {
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);
if (cookie) {
items = await getUserFeedItems(id, username, cookieJar, ctx.cache.tryGet);
} else {
items = [...userInfo.edge_felix_video_timeline.edges, ...userInfo.edge_owner_to_timeline_media.edges];
}
break;
}
case 'tags': {
@ -61,7 +71,7 @@ module.exports = async (ctx) => {
title: feedTitle,
link: feedLink,
description: feedDescription,
item: renderItems(items),
item: cookie ? renderItems(items) : renderGuestItems(items),
icon: `${baseUrl}/static/images/ico/xxhdpi_launcher.png/99cf3909d459.png`,
logo: feedLogo,
image: feedLogo,

View File

@ -1,5 +1,8 @@
const got = require('@/utils/got');
const { parseDate } = require('@/utils/parse-date');
const config = require('@/config').value;
const { art } = require('@/utils/render');
const path = require('path');
const baseUrl = 'https://www.instagram.com';
const COOKIE_URL = 'https://instagram.com';
@ -23,23 +26,30 @@ const getUserInfo = async (username, cookieJar, cache) => {
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');
try {
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);
} catch (e) {
if (e.message.includes("Cookie not in this host's domain")) {
throw Error('Invalid cookie');
}
throw e;
}
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;
@ -90,10 +100,71 @@ const getTagsFeedItems = (tag, tab, cookieJar, tryGet) =>
false
);
const renderGuestItems = (items) => {
const renderVideo = (node, summary) =>
art(path.join(__dirname, '../templates/video.art'), {
summary,
image: node.display_url,
video: {
url: node.video_url,
height: node.dimensions.height,
width: node.dimensions.width,
},
});
const renderImages = (node, summary) =>
art(path.join(__dirname, '../templates/images.art'), {
summary,
images: [{ url: node.display_url, height: node.dimensions.height, width: node.dimensions.width }],
});
return items.map(({ node }) => {
const type = node.__typename;
const summary = node.edge_media_to_caption.edges[0]?.node.text ?? '';
let description = '';
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('');
break;
case 'GraphVideo':
description = renderVideo(node, summary);
break;
case 'GraphImage':
description = renderImages(node, summary);
break;
default:
throw Error(`Instagram: Unhandled feed type: ${type}`);
}
return {
title: summary.split('\n')[0],
pubDate: parseDate(node.taken_at_timestamp, 'X'),
author: node.owner.username,
link: `${baseUrl}/p/${node.shortcode}/`,
summary,
description,
};
});
};
module.exports = {
baseUrl,
COOKIE_URL,
getUserInfo,
getUserFeedItems,
getTagsFeedItems,
renderGuestItems,
};

View File

@ -579,33 +579,35 @@ Type
## Instagram {#instagram}
:::warning
Due to Instagram Private API restrictions, you have to setup your credentials on the server. 2FA is not supported. See [deployment guide](https://docs.rsshub.app/install/) for more.
If you don't want to setup credentials, you can use [Picnob](#picnob) or [Picuki](#picuki).
:::
### User Profile / Hashtag - Private API {#instagram-user-profile-hashtag-private-api}
<Route author="oppilate DIYgod" example="/instagram/user/stefaniejoosten" path="/instagram/:category/:key" paramsDesc={['Feed category, see table below','Username / Hashtag name']} radar="1" anticrawler="1" selfhost="1">
| User timeline | Hashtag |
| ------------- | ------- |
| user | tags |
:::tip
It's highly recommended to deploy with Redis cache enabled.
:::
</Route>
### User Profile / Hashtag {#instagram-user-profile-hashtag-cookie}
### User Profile / Hashtag - Cookie {#instagram-user-profile-hashtag-cookie}
:::tip
<Route author="TonyRL" example="/instagram/2/user/stefaniejoosten" path="/instagram/2/:category/:key" paramsDesc={['Feed category, see table above','Username / Hashtag name']} radar="1" anticrawler="1" selfhost="1" />
You may need to setup cookie for a less restrictive rate limit and private profiles.
:::
<Route author="TonyRL" example="/instagram/2/user/stefaniejoosten" path="/instagram/2/:category/:key" paramsDesc={['Feed category, see table below','Username / Hashtag name']} radar="1" anticrawler="1" />
| User timeline | Hashtag |
| ------------- | ------- |
| user | tags |
### User Profile / Hashtag - Private API {#instagram-user-profile-hashtag-private-api}
:::warning
Due to [Instagram Private API](https://github.com/dilame/instagram-private-api) restrictions, you have to setup your credentials on the server. 2FA is not supported. See [deployment guide](https://docs.rsshub.app/install/) for more.
:::
<Route author="oppilate DIYgod" example="/instagram/user/stefaniejoosten" path="/instagram/:category/:key" paramsDesc={['Feed category, see table above','Username / Hashtag name']} radar="1" anticrawler="1" selfhost="1" />
## Keep {#keep}