mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-09-24 23:12:34 +08:00
feat(route): Twitter用户时间线和用户媒体时间线不再需要开发者账户 (#7739)
This commit is contained in:
+14
-4
@@ -85,17 +85,27 @@
|
||||
docs: 'https://docs.rsshub.app/social-media.html#twitter',
|
||||
source: '/:id',
|
||||
target: (params) => {
|
||||
if (params.id !== 'home' && params.id !== 'explore' && params.id !== 'notifications' && params.id !== 'messages' && params.id !== 'explore' && params.id !== 'search') {
|
||||
if (!['home', 'explore', 'notifications', 'messages', 'explore', 'search'].includes(params.id)) {
|
||||
return '/twitter/user/:id';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户媒体时间线',
|
||||
docs: 'https://docs.rsshub.app/social-media.html#twitter',
|
||||
source: '/:id/media',
|
||||
target: (params) => {
|
||||
if (!['home', 'explore', 'notifications', 'messages', 'explore', 'search'].includes(params.id)) {
|
||||
return '/twitter/media/:id';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户关注时间线',
|
||||
docs: 'https://docs.rsshub.app/social-media.html#twitter',
|
||||
source: '/:id',
|
||||
target: (params) => {
|
||||
if (params.id !== 'home' && params.id !== 'explore' && params.id !== 'notifications' && params.id !== 'messages' && params.id !== 'explore' && params.id !== 'search') {
|
||||
if (!['home', 'explore', 'notifications', 'messages', 'explore', 'search'].includes(params.id)) {
|
||||
return '/twitter/followings/:id';
|
||||
}
|
||||
},
|
||||
@@ -105,7 +115,7 @@
|
||||
docs: 'https://docs.rsshub.app/social-media.html#twitter',
|
||||
source: '/:id',
|
||||
target: (params) => {
|
||||
if (params.id !== 'home' && params.id !== 'explore' && params.id !== 'notifications' && params.id !== 'messages' && params.id !== 'explore' && params.id !== 'search') {
|
||||
if (!['home', 'explore', 'notifications', 'messages', 'explore', 'search'].includes(params.id)) {
|
||||
return '/twitter/likes/:id';
|
||||
}
|
||||
},
|
||||
@@ -115,7 +125,7 @@
|
||||
docs: 'https://docs.rsshub.app/social-media.html#twitter',
|
||||
source: '/:id/lists/:name',
|
||||
target: (params) => {
|
||||
if (params.id !== 'home' && params.id !== 'explore' && params.id !== 'notifications' && params.id !== 'messages' && params.id !== 'explore' && params.id !== 'search') {
|
||||
if (!['home', 'explore', 'notifications', 'messages', 'explore', 'search'].includes(params.id)) {
|
||||
return '/twitter/list/:id/:name';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -298,6 +298,10 @@ generates
|
||||
|
||||
<RouteEn path="/twitter/user/:id/:routeParams?" example="/twitter/user/DIYgod" :paramsDesc="['user id', 'extra parameters, see the table above; particularly when `routeParams=exclude_replies`, replies are excluded; `routeParams=exclude_rts` excludes retweets,`routeParams=exclude_rts_replies` exclude replies and retweets; for default include all.']" radar="1" rssbud="1"/>
|
||||
|
||||
### User media
|
||||
|
||||
<RouteEn author="yindaheng98" path="/twitter/media/:id/:routeParams?" example="/twitter/media/DIYgod" :paramsDesc="['user id', 'extra parameters, see the table above.']" radar="1" rssbud="1"/>
|
||||
|
||||
## User following timeline
|
||||
|
||||
<RouteEn author="DIYgod" example="/twitter/followings/DIYgod" path="/twitter/followings/:id/:routeParams?" :paramsDesc="['user id', 'extra parameters, see the table above']" radar="1" rssbud="1" selfhost="1">
|
||||
|
||||
@@ -651,6 +651,10 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
|
||||
|
||||
<Route author="DIYgod" example="/twitter/user/DIYgod" path="/twitter/user/:id/:routeParams?" :paramsDesc="['用户名', '额外参数;请参阅上面的说明和表格;特别地,当 `routeParams=exclude_replies`时去除回复,`routeParams=exclude_rts`去除转推,`routeParams=exclude_rts_replies`去除回复和转推,默认包含全部回复和转推。']" radar="1" rssbud="1"/>
|
||||
|
||||
### 用户媒体时间线
|
||||
|
||||
<Route author="yindaheng98" example="/twitter/media/DIYgod" path="/twitter/media/:id/:routeParams?" :paramsDesc="['用户名', '额外参数;请参阅上面的说明和表格。']" radar="1" rssbud="1"/>
|
||||
|
||||
### 用户关注时间线
|
||||
|
||||
<Route author="DIYgod" example="/twitter/followings/DIYgod" path="/twitter/followings/:id/:routeParams?" :paramsDesc="['用户名', '额外参数;请参阅上面的说明和表格']" radar="1" rssbud="1" selfhost="1">
|
||||
@@ -673,7 +677,7 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
|
||||
|
||||
### 关键词
|
||||
|
||||
<Route author="DIYgod" example="/twitter/keyword/RSSHub" path="/twitter/keyword/:keyword/:routeParams?" :paramsDesc="['关键词', '额外参数;请参阅上面的说明和表格']" radar="1" rssbud="1"/>
|
||||
<Route author="DIYgod" example="/twitter/keyword/RSSHub" path="/twitter/keyword/:keyword/:routeParams?/limit?" :paramsDesc="['关键词', '额外参数;请参阅上面的说明和表格', '查询前多少条']" radar="1" rssbud="1"/>
|
||||
|
||||
### Trends
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ const calculateValue = () => {
|
||||
consumer_key: envs.TWITTER_CONSUMER_KEY,
|
||||
consumer_secret: envs.TWITTER_CONSUMER_SECRET,
|
||||
tokens: twitter_tokens,
|
||||
authorization: envs.TWITTER_WEBAPI_AUTHORIZAION || 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
|
||||
// 此处硬编码参考自https://github.com/mikf/gallery-dl/blob/master/gallery_dl/extractor/twitter.py#L550
|
||||
},
|
||||
youtube: {
|
||||
key: envs.YOUTUBE_KEY,
|
||||
|
||||
+2
-1
@@ -223,8 +223,9 @@ router.get('/twitter/user/:id/:routeParams?', lazyloadRouteHandler('./routes/twi
|
||||
router.get('/twitter/list/:id/:name/:routeParams?', lazyloadRouteHandler('./routes/twitter/list'));
|
||||
router.get('/twitter/likes/:id/:routeParams?', lazyloadRouteHandler('./routes/twitter/likes'));
|
||||
router.get('/twitter/followings/:id/:routeParams?', lazyloadRouteHandler('./routes/twitter/followings'));
|
||||
router.get('/twitter/keyword/:keyword/:routeParams?', lazyloadRouteHandler('./routes/twitter/keyword'));
|
||||
router.get('/twitter/keyword/:keyword/:routeParams?/:limit?', lazyloadRouteHandler('./routes/twitter/keyword'));
|
||||
router.get('/twitter/trends/:woeid?', lazyloadRouteHandler('./routes/twitter/trends'));
|
||||
router.get('/twitter/media/:id/:routeParams?', lazyloadRouteHandler('./routes/twitter/media'));
|
||||
|
||||
// YouTube
|
||||
router.get('/youtube/user/:username/:embed?', lazyloadRouteHandler('./routes/youtube/user'));
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const utils = require('../utils');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const keyword = ctx.params.keyword;
|
||||
const limit = ctx.params.limit || 50;
|
||||
const result = await utils.getTwit().get('search/tweets', {
|
||||
q: keyword,
|
||||
count: limit,
|
||||
tweet_mode: 'extended',
|
||||
result_type: 'recent',
|
||||
});
|
||||
const data = result.data;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter Keyword - ${keyword}`,
|
||||
link: `https://twitter.com/search?q=${encodeURIComponent(keyword)}`,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data: data.statuses,
|
||||
}),
|
||||
allowEmpty: true,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const utils = require('../utils');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const id = ctx.params.id;
|
||||
|
||||
// For compatibility
|
||||
const { exclude_replies, include_rts, count } = utils.parseRouteParams(ctx.params.routeParams);
|
||||
const result = await utils.getTwit().get('statuses/user_timeline', {
|
||||
screen_name: id,
|
||||
tweet_mode: 'extended',
|
||||
exclude_replies,
|
||||
include_rts,
|
||||
count,
|
||||
});
|
||||
const data = result.data;
|
||||
const userInfo = data[0].user;
|
||||
|
||||
const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter @${userInfo.name}`,
|
||||
link: `https://twitter.com/${id}`,
|
||||
image: profileImageUrl,
|
||||
description: userInfo.description,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -1,25 +1,11 @@
|
||||
const utils = require('./utils');
|
||||
const config = require('@/config').value;
|
||||
const webApiImpl = require('./web-api/search');
|
||||
const devApiImpl = require('./developer-api/search');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
if (!config.twitter || !config.twitter.consumer_key || !config.twitter.consumer_secret) {
|
||||
throw 'Twitter RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#%E9%83%A8%E5%88%86-rss-%E6%A8%A1%E5%9D%97%E9%85%8D%E7%BD%AE">relevant config</a>';
|
||||
await webApiImpl(ctx);
|
||||
} else {
|
||||
await devApiImpl(ctx);
|
||||
}
|
||||
const keyword = ctx.params.keyword;
|
||||
const result = await utils.getTwit().get('search/tweets', {
|
||||
q: keyword,
|
||||
count: 50,
|
||||
tweet_mode: 'extended',
|
||||
result_type: 'recent',
|
||||
});
|
||||
const data = result.data;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter Keyword - ${keyword}`,
|
||||
link: `https://twitter.com/search?q=${encodeURIComponent(keyword)}`,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data: data.statuses,
|
||||
}),
|
||||
allowEmpty: true,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// const config = require('@/config').value;
|
||||
// const devApiImpl = require('./developer-api/user');
|
||||
const webApiImpl = require('./web-api/media');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
await webApiImpl(ctx);
|
||||
};
|
||||
@@ -1,54 +1,11 @@
|
||||
const querystring = require('querystring');
|
||||
const utils = require('./utils');
|
||||
const config = require('@/config').value;
|
||||
const { fallback, queryToInteger, queryToBoolean } = require('@/utils/readable-social');
|
||||
const devApiImpl = require('./developer-api/user');
|
||||
const webApiImpl = require('./web-api/user');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
if (!config.twitter || !config.twitter.consumer_key || !config.twitter.consumer_secret) {
|
||||
throw 'Twitter RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#%E9%83%A8%E5%88%86-rss-%E6%A8%A1%E5%9D%97%E9%85%8D%E7%BD%AE">relevant config</a>';
|
||||
}
|
||||
|
||||
const id = ctx.params.id;
|
||||
const routeParams = ctx.params.routeParams;
|
||||
let exclude_replies = false;
|
||||
let include_rts = true;
|
||||
|
||||
// For compatibility
|
||||
let count = undefined;
|
||||
if (routeParams === 'exclude_rts_replies' || routeParams === 'exclude_replies_rts') {
|
||||
exclude_replies = true;
|
||||
include_rts = false;
|
||||
} else if (routeParams === 'exclude_replies') {
|
||||
exclude_replies = true;
|
||||
include_rts = true;
|
||||
} else if (routeParams === 'exclude_rts') {
|
||||
exclude_replies = false;
|
||||
include_rts = false;
|
||||
await webApiImpl(ctx);
|
||||
} else {
|
||||
const parsed = querystring.parse(routeParams);
|
||||
count = fallback(undefined, queryToInteger(parsed.count), undefined);
|
||||
exclude_replies = fallback(undefined, queryToBoolean(parsed.excludeReplies), false);
|
||||
include_rts = fallback(undefined, queryToBoolean(parsed.includeRts), true);
|
||||
await devApiImpl(ctx);
|
||||
}
|
||||
|
||||
const result = await utils.getTwit().get('statuses/user_timeline', {
|
||||
screen_name: id,
|
||||
tweet_mode: 'extended',
|
||||
exclude_replies,
|
||||
include_rts,
|
||||
count,
|
||||
});
|
||||
const data = result.data;
|
||||
const userInfo = data[0].user;
|
||||
const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter @${userInfo.name}`,
|
||||
link: `https://twitter.com/${id}/`,
|
||||
image: profileImageUrl,
|
||||
description: userInfo.description,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+46
-23
@@ -93,7 +93,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
if (!readable) {
|
||||
content += '<br>';
|
||||
}
|
||||
content += `<video src="${video.url}" ${gifAutoPlayAttr} controls="controls" poster="${getOriginalImg(media.media_url_https)}" ${extraAttrs}></video>`;
|
||||
content += `<video src='${video.url}' ${gifAutoPlayAttr} controls='controls' poster='${getOriginalImg(media.media_url_https)}' ${extraAttrs}></video>`;
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -120,7 +120,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
content += `<br>`;
|
||||
}
|
||||
if (addLinkForPics) {
|
||||
content += `<a href="${originalImg}" target="_blank" rel="noopener noreferrer">`;
|
||||
content += `<a href='${originalImg}' target='_blank' rel='noopener noreferrer'>`;
|
||||
}
|
||||
content += `<img `;
|
||||
if (widthOfPics >= 0) {
|
||||
@@ -142,7 +142,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
});
|
||||
|
||||
if (readable && img) {
|
||||
img = `<br clear="both" /><div style="clear: both"></div>` + img;
|
||||
img = `<br clear='both' /><div style='clear: both'></div>` + img;
|
||||
}
|
||||
return img;
|
||||
};
|
||||
@@ -162,7 +162,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
case 'photo':
|
||||
default:
|
||||
originalImg = getOriginalImg(item.media_url_https);
|
||||
content = `<img width="0" height="0" hidden="true" src="${originalImg}">`;
|
||||
content = `<img width='0' height='0' hidden='true' src='${originalImg}'>`;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -172,14 +172,15 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
};
|
||||
const formatUrl = (item) => {
|
||||
let url = '';
|
||||
item.entities.urls.forEach((u) => {
|
||||
if (readable) {
|
||||
url += '<br>';
|
||||
} else {
|
||||
url += ' ';
|
||||
}
|
||||
url += `<a href="${u.expanded_url}" target="_blank" rel="noopener noreferrer">${u.expanded_url}</a>`;
|
||||
});
|
||||
item.entities.urls &&
|
||||
item.entities.urls.forEach((u) => {
|
||||
if (readable) {
|
||||
url += '<br>';
|
||||
} else {
|
||||
url += ' ';
|
||||
}
|
||||
url += `<a href='${u.expanded_url}' target='_blank' rel='noopener noreferrer'>${u.expanded_url}</a>`;
|
||||
});
|
||||
|
||||
return url;
|
||||
};
|
||||
@@ -201,18 +202,18 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
if (quoteData) {
|
||||
const author = quoteData.user;
|
||||
if (readable) {
|
||||
quote += `<br clear="both" /><div style="clear: both"></div>`;
|
||||
quote += `<blockquote style="background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;">`;
|
||||
quote += `<br clear='both' /><div style='clear: both'></div>`;
|
||||
quote += `<blockquote style='background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;'>`;
|
||||
} else {
|
||||
quote += `<br><br>`;
|
||||
}
|
||||
|
||||
if (readable) {
|
||||
quote += `<a href="https://twitter.com/${author.screen_name}" target="_blank" rel="noopener noreferrer">`;
|
||||
quote += `<a href='https://twitter.com/${author.screen_name}' target='_blank' rel='noopener noreferrer'>`;
|
||||
}
|
||||
|
||||
if (showQuotedAuthorAvatarInDesc) {
|
||||
quote += `<img width="${sizeOfQuotedAuthorAvatar}" height="${sizeOfQuotedAuthorAvatar}" src="${author.profile_image_url_https}" ${readable ? 'hspace="8" vspace="8" align="left"' : ''}>`;
|
||||
quote += `<img width='${sizeOfQuotedAuthorAvatar}' height='${sizeOfQuotedAuthorAvatar}' src='${author.profile_image_url_https}' ${readable ? 'hspace="8" vspace="8" align="left"' : ''}>`;
|
||||
}
|
||||
|
||||
if (authorNameBold) {
|
||||
@@ -242,13 +243,13 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
quoteInTitle += `${author.name}: ${formatTextToPlain(quoteData.full_text)}`;
|
||||
|
||||
if (readable) {
|
||||
quote += `<br><small>Link: <a href="https://twitter.com/${author.screen_name}/status/${quoteData.id_str}" target="_blank" rel="noopener noreferrer">https://twitter.com/${author.screen_name}/status/${quoteData.id_str}</a></small>`;
|
||||
quote += `<br><small>Link: <a href='https://twitter.com/${author.screen_name}/status/${quoteData.id_str}' target='_blank' rel='noopener noreferrer'>https://twitter.com/${author.screen_name}/status/${quoteData.id_str}</a></small>`;
|
||||
}
|
||||
if (showTimestampInDescription) {
|
||||
quote += '<br><small>' + new Date(quoteData.created_at).toLocaleString();
|
||||
quote += `</small>`;
|
||||
if (readable) {
|
||||
quote += `<br clear="both" /><div style="clear: both"></div>`;
|
||||
quote += `<br clear='both' /><div style='clear: both'></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +298,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
if (showAuthorInDesc) {
|
||||
if (readable) {
|
||||
description += '<small>';
|
||||
description += `<a href="https://twitter.com/${originalItem.user.screen_name}" target="_blank" rel="noopener noreferrer">`;
|
||||
description += `<a href='https://twitter.com/${originalItem.user.screen_name}' target='_blank' rel='noopener noreferrer'>`;
|
||||
}
|
||||
if (authorNameBold) {
|
||||
description += `<strong>`;
|
||||
@@ -315,7 +316,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
if (!showAuthorInDesc) {
|
||||
description += ' ';
|
||||
if (readable) {
|
||||
description += `<a href="https://twitter.com/${item.user.screen_name}" target="_blank" rel="noopener noreferrer">`;
|
||||
description += `<a href='https://twitter.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`;
|
||||
}
|
||||
if (authorNameBold) {
|
||||
description += `<strong>`;
|
||||
@@ -335,11 +336,11 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
}
|
||||
if (showAuthorInDesc) {
|
||||
if (readable) {
|
||||
description += `<a href="https://twitter.com/${item.user.screen_name}" target="_blank" rel="noopener noreferrer">`;
|
||||
description += `<a href='https://twitter.com/${item.user.screen_name}' target='_blank' rel='noopener noreferrer'>`;
|
||||
}
|
||||
|
||||
if (showAuthorAvatarInDesc) {
|
||||
description += `<img width="${sizeOfAuthorAvatar}" height="${sizeOfAuthorAvatar}" src="${item.user.profile_image_url_https}" ${readable ? 'hspace="8" vspace="8" align="left"' : ''}>`;
|
||||
description += `<img width='${sizeOfAuthorAvatar}' height='${sizeOfAuthorAvatar}' src='${item.user.profile_image_url_https}' ${readable ? 'hspace="8" vspace="8" align="left"' : ''}>`;
|
||||
}
|
||||
if (authorNameBold) {
|
||||
description += `<strong>`;
|
||||
@@ -363,7 +364,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
|
||||
description += quote;
|
||||
|
||||
if (readable) {
|
||||
description += `<br clear="both" /><div style="clear: both"></div><hr>`;
|
||||
description += `<br clear='both' /><div style='clear: both'></div><hr>`;
|
||||
}
|
||||
|
||||
if (showTimestampInDescription) {
|
||||
@@ -407,7 +408,29 @@ if (config.twitter.consumer_key && config.twitter.consumer_secret) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseRouteParams(routeParams) {
|
||||
let exclude_replies, include_rts;
|
||||
let count = undefined;
|
||||
if (routeParams === 'exclude_rts_replies' || routeParams === 'exclude_replies_rts') {
|
||||
exclude_replies = true;
|
||||
include_rts = false;
|
||||
} else if (routeParams === 'exclude_replies') {
|
||||
exclude_replies = true;
|
||||
include_rts = true;
|
||||
} else if (routeParams === 'exclude_rts') {
|
||||
exclude_replies = false;
|
||||
include_rts = false;
|
||||
} else {
|
||||
const parsed = querystring.parse(routeParams);
|
||||
count = fallback(undefined, queryToInteger(parsed.count), undefined);
|
||||
exclude_replies = fallback(undefined, queryToBoolean(parsed.excludeReplies), false);
|
||||
include_rts = fallback(undefined, queryToBoolean(parsed.includeRts), true);
|
||||
}
|
||||
return { count, exclude_replies, include_rts };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ProcessFeed,
|
||||
getTwit,
|
||||
parseRouteParams,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const utils = require('../utils');
|
||||
// const config = require('@/config').value;
|
||||
const { getUser, getUserMedia } = require('./twitter-api');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const id = ctx.params.id;
|
||||
const userInfo = await getUser(ctx.cache, id);
|
||||
const data = await getUserMedia(ctx.cache, id);
|
||||
const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter @${userInfo.name}`,
|
||||
link: `https://twitter.com/${id}/media`,
|
||||
image: profileImageUrl,
|
||||
description: userInfo.description,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const utils = require('../utils');
|
||||
const { getSearch } = require('./twitter-api');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const keyword = ctx.params.keyword;
|
||||
const data = await getSearch(keyword);
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter Keyword - ${keyword}`,
|
||||
link: `https://twitter.com/search?q=${encodeURIComponent(keyword)}`,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data,
|
||||
}),
|
||||
allowEmpty: true,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
const twitterGot = require('./twitter-got');
|
||||
const _params = {
|
||||
count: 20,
|
||||
include_profile_interstitial_type: 1,
|
||||
include_blocking: 1,
|
||||
include_blocked_by: 1,
|
||||
include_followed_by: 1,
|
||||
include_want_retweets: 1,
|
||||
include_mute_edge: 1,
|
||||
include_can_dm: 1,
|
||||
include_can_media_tag: 1,
|
||||
skip_status: 1,
|
||||
cards_platform: 'Web-12',
|
||||
include_cards: 1,
|
||||
include_ext_alt_text: true,
|
||||
include_quote_count: true,
|
||||
include_reply_count: 1,
|
||||
tweet_mode: 'extended',
|
||||
include_entities: true,
|
||||
include_user_entities: true,
|
||||
include_ext_media_color: true,
|
||||
include_ext_media_availability: true,
|
||||
send_error_codes: true,
|
||||
simple_quoted_tweet: true,
|
||||
include_tweet_replies: false,
|
||||
ext: 'mediaStats,highlightedLabel',
|
||||
};
|
||||
|
||||
const pagination = (endpoint, userId, params) =>
|
||||
twitterGot('https://api.twitter.com' + endpoint, {
|
||||
..._params,
|
||||
...params,
|
||||
userId,
|
||||
});
|
||||
const timelineMedia = (userId, params = {}) => pagination(`/2/timeline/media/${userId}.json`, userId, params);
|
||||
const timelineTweets = (userId, params = {}) => pagination(`/2/timeline/profile/${userId}.json`, userId, params);
|
||||
const timelineTweetsAndReplies = (userId, params = {}) =>
|
||||
pagination(`/2/timeline/profile/${userId}.json`, userId, {
|
||||
...params,
|
||||
include_tweet_replies: true,
|
||||
});
|
||||
const timelineLikes = (userId, params = {}) =>
|
||||
pagination(`/2/timeline/favorites/${userId}.json`, userId, {
|
||||
...params,
|
||||
sorted_by_time: true,
|
||||
});
|
||||
const timelineKeywords = (keywords, params = {}) =>
|
||||
twitterGot('https://twitter.com/i/api/2/search/adaptive.json', {
|
||||
..._params,
|
||||
...params,
|
||||
q: keywords,
|
||||
query_source: 'typed_query',
|
||||
pc: 1,
|
||||
});
|
||||
|
||||
function pickLegacyByID(id, tweets_dict, users_dict) {
|
||||
function pickLegacyFromTweet(tweet) {
|
||||
tweet.user = users_dict[tweet.user_id_str];
|
||||
if (tweet.retweeted_status_id_str) {
|
||||
tweet.retweeted_status = pickLegacyFromTweet(tweets_dict[tweet.retweeted_status_id_str]);
|
||||
}
|
||||
return tweet;
|
||||
}
|
||||
|
||||
if (tweets_dict[id]) {
|
||||
return pickLegacyFromTweet(tweets_dict[id]);
|
||||
}
|
||||
}
|
||||
|
||||
function gatherLegacyFromData(data, filter = 'tweet-') {
|
||||
const tweets_dict = data.globalObjects.tweets;
|
||||
const users_dict = data.globalObjects.users;
|
||||
const tweets = [];
|
||||
data.timeline.instructions[0].addEntries.entries.forEach((entry) => {
|
||||
if (entry.entryId && entry.entryId.indexOf(filter) !== -1) {
|
||||
const tweet = pickLegacyByID(entry.content.item.content.tweet.id, tweets_dict, users_dict);
|
||||
if (tweet) {
|
||||
tweets.push(tweet);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tweets;
|
||||
}
|
||||
|
||||
const getUserTweetsByID = async (id, params = {}) => gatherLegacyFromData(await timelineTweets(id, params));
|
||||
const getUserTweetsAndRepliesByID = async (id, params = {}) => gatherLegacyFromData(await timelineTweetsAndReplies(id, params));
|
||||
const getUserMediaByID = async (id, params = {}) => gatherLegacyFromData(await timelineMedia(id, params));
|
||||
const getUserLikesByID = async (id, params = {}) => gatherLegacyFromData(await timelineLikes(id, params));
|
||||
|
||||
const excludeRetweet = function (tweets) {
|
||||
const excluded = [];
|
||||
for (const t of tweets) {
|
||||
if (t.retweeted_status) {
|
||||
continue;
|
||||
}
|
||||
excluded.push(t);
|
||||
}
|
||||
return excluded;
|
||||
};
|
||||
|
||||
const userByScreenName = (screenName) =>
|
||||
twitterGot('https://twitter.com/i/api/graphql/hc-pka9A7gyS3xODIafnrQ/UserByScreenName', {
|
||||
variables: `{"screen_name":"${screenName}","withHighlightedLabel":true}`,
|
||||
});
|
||||
const getUserData = (cache, screenName) => cache.tryGet(`twitter-userdata-${screenName}`, () => userByScreenName(screenName));
|
||||
const getUserID = async (cache, screenName) => (await getUserData(cache, screenName)).data.user.rest_id;
|
||||
const getUser = async (cache, screenName) => (await getUserData(cache, screenName)).data.user.legacy;
|
||||
|
||||
const getUserTweets = async (cache, screenName, params = {}) => getUserTweetsByID(await getUserID(cache, screenName), params);
|
||||
const getUserTweetsAndReplies = async (cache, screenName, params = {}) => getUserTweetsAndRepliesByID(await getUserID(cache, screenName), params);
|
||||
const getUserMedia = async (cache, screenName, params = {}) => getUserMediaByID(await getUserID(cache, screenName), params);
|
||||
const getUserLikes = async (cache, screenName, params = {}) => getUserLikesByID(await getUserID(cache, screenName), params);
|
||||
const getSearch = async (keywords, params = {}) => gatherLegacyFromData(await timelineKeywords(keywords, params), 'sq-I-t-');
|
||||
|
||||
module.exports = {
|
||||
getUser,
|
||||
getUserTweets,
|
||||
getUserTweetsAndReplies,
|
||||
getUserMedia,
|
||||
getUserLikes,
|
||||
excludeRetweet,
|
||||
getSearch,
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
const { CookieJar, Cookie } = require('tough-cookie');
|
||||
const { promisify } = require('util');
|
||||
const queryString = require('query-string');
|
||||
const got = require('@/utils/got');
|
||||
const config = require('@/config').value;
|
||||
const headers = {
|
||||
authorization: config.twitter.authorization,
|
||||
'x-guest-token': undefined,
|
||||
'x-twitter-auth-type': undefined,
|
||||
'x-twitter-client-language': 'en',
|
||||
'x-twitter-active-user': 'yes',
|
||||
'x-csrf-token': undefined,
|
||||
};
|
||||
let cookieJar, setCookie, getCookies;
|
||||
|
||||
const cookiedomain = 'twitter.com';
|
||||
const cookieurl = 'https://twitter.com';
|
||||
|
||||
async function twitterGot(options) {
|
||||
const response = await got({
|
||||
...options,
|
||||
headers: { ...headers, ...(options.headers || {}) },
|
||||
cookieJar,
|
||||
});
|
||||
// 更新csrfToken
|
||||
for (const c of await getCookies(cookieurl)) {
|
||||
if (c.key === 'ct0') {
|
||||
headers['x-csrf-token'] = c.value;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function resetSession() {
|
||||
cookieJar = new CookieJar();
|
||||
getCookies = promisify(cookieJar.getCookies.bind(cookieJar));
|
||||
setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
|
||||
// 生成csrf-token
|
||||
const csrfToken = [...Array(16 * 2)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
await setCookie(new Cookie({ key: 'ct0', value: csrfToken, domain: cookiedomain, secure: false }), cookieurl);
|
||||
headers['x-csrf-token'] = csrfToken;
|
||||
headers['x-guest-token'] = undefined;
|
||||
// 发起初始化请求
|
||||
const response = await twitterGot({
|
||||
url: 'https://api.twitter.com/1.1/guest/activate.json',
|
||||
method: 'POST',
|
||||
});
|
||||
// 获取guest-token
|
||||
// TODO: OAuth2Session, 参见 https://github.com/DIYgod/RSSHub/pull/7739#discussionR655932602
|
||||
const guestToken = response.data.guest_token;
|
||||
headers['x-guest-token'] = guestToken;
|
||||
await setCookie(new Cookie({ key: 'gt', value: guestToken, domain: cookiedomain, secure: false }), cookieurl);
|
||||
// 发起第二个初始化请求, 获取_twitter_sess
|
||||
await twitterGot({
|
||||
url: 'https://twitter.com/i/js_inst',
|
||||
method: 'GET',
|
||||
searchParams: queryString.stringify({ c_name: 'ui_metrics' }),
|
||||
});
|
||||
return cookieJar;
|
||||
}
|
||||
|
||||
const initSession = () => cookieJar || resetSession();
|
||||
|
||||
async function twitterRequest(url, params, method) {
|
||||
await initSession();
|
||||
// 发起请求
|
||||
const request = () =>
|
||||
twitterGot({
|
||||
url,
|
||||
method,
|
||||
searchParams: queryString.stringify(params),
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await request();
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
await resetSession();
|
||||
response = await request();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
module.exports = twitterRequest;
|
||||
@@ -0,0 +1,32 @@
|
||||
const utils = require('../utils');
|
||||
const { getUser, getUserTweets, getUserTweetsAndReplies, excludeRetweet } = require('./twitter-api');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const id = ctx.params.id;
|
||||
|
||||
// For compatibility
|
||||
const { exclude_replies, include_rts } = utils.parseRouteParams(ctx.params.routeParams);
|
||||
|
||||
let data;
|
||||
const userInfo = await getUser(ctx.cache, id);
|
||||
if (exclude_replies) {
|
||||
data = await getUserTweets(ctx.cache, id);
|
||||
} else {
|
||||
data = await getUserTweetsAndReplies(ctx.cache, id);
|
||||
}
|
||||
if (!include_rts) {
|
||||
data = excludeRetweet(data);
|
||||
}
|
||||
|
||||
const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Twitter @${userInfo.name}`,
|
||||
link: `https://twitter.com/${id}`,
|
||||
image: profileImageUrl,
|
||||
description: userInfo.description,
|
||||
item: utils.ProcessFeed(ctx, {
|
||||
data,
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user