feat(route/twitter): use unique ID in some routes (#12759)

Signed-off-by: Rongrong <i@rong.moe>
This commit is contained in:
Rongrong 2023-07-06 00:06:48 +08:00 committed by GitHub
parent ef497881a7
commit c771fbd3cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 62 additions and 29 deletions

View File

@ -494,15 +494,15 @@ generates
### User timeline
<RouteEn author="DIYgod yindaheng98 Rongronggg9" 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"/>
<RouteEn author="DIYgod yindaheng98 Rongronggg9" path="/twitter/user/:id/:routeParams?" example="/twitter/user/DIYgod" :paramsDesc="['username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', '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 Rongronggg9" path="/twitter/media/:id/:routeParams?" example="/twitter/media/DIYgod" :paramsDesc="['user id', 'extra parameters, see the table above.']" radar="1" rssbud="1"/>
<RouteEn author="yindaheng98 Rongronggg9" path="/twitter/media/:id/:routeParams?" example="/twitter/media/DIYgod" :paramsDesc="['username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', '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">
<RouteEn author="DIYgod" example="/twitter/followings/DIYgod" path="/twitter/followings/:id/:routeParams?" :paramsDesc="['username', 'extra parameters, see the table above']" radar="1" rssbud="1" selfhost="1">
::: warning
@ -514,11 +514,11 @@ This route requires Twitter token's corresponding id, therefore it's only availa
### List timeline
<RouteEn author="xyqfer" example="/twitter/list/ladyleet/javascript" path="/twitter/list/:id/:name/:routeParams?" :paramsDesc="['user name', 'list name', 'extra parameters, see the table above']" radar="1" rssbud="1"/>
<RouteEn author="xyqfer" example="/twitter/list/ladyleet/javascript" path="/twitter/list/:id/:name/:routeParams?" :paramsDesc="['username', 'list name', 'extra parameters, see the table above']" radar="1" rssbud="1"/>
### User likes
<RouteEn author="xyqfer" example="/twitter/likes/DIYgod" path="/twitter/likes/:id/:routeParams?" :paramsDesc="['user name', 'extra parameters, see the table above']" radar="1" rssbud="1"/>
<RouteEn author="xyqfer" example="/twitter/likes/DIYgod" path="/twitter/likes/:id/:routeParams?" :paramsDesc="['username', 'extra parameters, see the table above']" radar="1" rssbud="1"/>
### Keyword
@ -530,7 +530,7 @@ This route requires Twitter token's corresponding id, therefore it's only availa
### Collection
<RouteEn author="TonyRL" example="/twitter/collection/DIYgod/1527857429467172864" path="/twitter/collection/:uid/:collectionId/:routeParams?" :paramsDesc="['User name, should match the generated token', 'Collection ID, can be found in URL', 'extra parameters, see the table above']" radar="1" rssbud="1" selfhost="1">
<RouteEn author="TonyRL" example="/twitter/collection/DIYgod/1527857429467172864" path="/twitter/collection/:uid/:collectionId/:routeParams?" :paramsDesc="['username, should match the generated token', 'collection ID, can be found in URL', 'extra parameters, see the table above']" radar="1" rssbud="1" selfhost="1">
::: warning
@ -542,7 +542,7 @@ This route requires Twitter token's corresponding id, therefore it's only availa
### Tweet Details
<Route author="LarchLiu Rongronggg9" example="/twitter/tweet/DIYgod/status/1650844643997646852" path="/twitter/tweet/:id/status/:status/:original?" :paramsDesc="['User name', 'Tweet ID', 'extra parameters, data type of return, if the value is not `0`/`false` and `config.isPackage` is `true`, return the original data of twitter']" radar="1" rssbud="1"/>
<Route author="LarchLiu Rongronggg9" example="/twitter/tweet/DIYgod/status/1650844643997646852" path="/twitter/tweet/:id/status/:status/:original?" :paramsDesc="['username; in particular, if starts with `+`, it will be recognized as a [unique ID](https://github.com/DIYgod/RSSHub/issues/12221), e.g. `+44196397`', 'tweet ID', 'extra parameters, data type of return, if the value is not `0`/`false` and `config.isPackage` is `true`, return the original data of twitter']" radar="1" rssbud="1"/>
## Vimeo

View File

@ -832,11 +832,11 @@ Instagram Stories 没有可靠的 guid你的 RSS 阅读器可能将同一条
### 用户时间线
<Route author="DIYgod yindaheng98 Rongronggg9" 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="DIYgod yindaheng98 Rongronggg9" example="/twitter/user/DIYgod" path="/twitter/user/:id/:routeParams?" :paramsDesc="['用户名;特别地,以 `+` 开头则代表[唯一 ID](https://github.com/DIYgod/RSSHub/issues/12221),如 `+44196397`', '额外参数;请参阅上面的说明和表格;特别地,当 `routeParams=exclude_replies`时去除回复,`routeParams=exclude_rts`去除转推,`routeParams=exclude_rts_replies`去除回复和转推,默认包含全部回复和转推。']" radar="1" rssbud="1"/>
### 用户媒体时间线
<Route author="yindaheng98 Rongronggg9" example="/twitter/media/DIYgod" path="/twitter/media/:id/:routeParams?" :paramsDesc="['用户名', '额外参数;请参阅上面的说明和表格。']" radar="1" rssbud="1"/>
<Route author="yindaheng98 Rongronggg9" example="/twitter/media/DIYgod" path="/twitter/media/:id/:routeParams?" :paramsDesc="['用户名;特别地,以 `+` 开头则代表[唯一 ID](https://github.com/DIYgod/RSSHub/issues/12221),如 `+44196397`', '额外参数;请参阅上面的说明和表格。']" radar="1" rssbud="1"/>
### 用户关注时间线
@ -880,7 +880,7 @@ Instagram Stories 没有可靠的 guid你的 RSS 阅读器可能将同一条
### 推文详情
<Route author="LarchLiu Rongronggg9" example="/twitter/tweet/DIYgod/status/1650844643997646852" path="/twitter/tweet/:id/status/:status/:original?" :paramsDesc="['用户名', '推文 ID', '额外参数;返回数据类型,当非 `0`/`false` 且 `config.isPackage``true`时,返回 twitter 原始数据']" radar="1" rssbud="1"/>
<Route author="LarchLiu Rongronggg9" example="/twitter/tweet/DIYgod/status/1650844643997646852" path="/twitter/tweet/:id/status/:status/:original?" :paramsDesc="['用户名;特别地,以 `+` 开头则代表[唯一 ID](https://github.com/DIYgod/RSSHub/issues/12221),如 `+44196397`', '推文 ID', '额外参数;返回数据类型,当非 `0`/`false` 且 `config.isPackage``true`时,返回 twitter 原始数据']" radar="1" rssbud="1"/>
## Vimeo

View File

@ -2,24 +2,32 @@ 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 client = await utils.getAppClient();
const data = await client.v1.get('statuses/user_timeline.json', {
screen_name: id,
const user_timeline_query = {
tweet_mode: 'extended',
exclude_replies,
include_rts,
count,
});
};
let screen_name;
if (id.startsWith('+')) {
user_timeline_query.user_id = +id.slice(1);
} else {
user_timeline_query.screen_name = id;
screen_name = id;
}
const data = await client.v1.get('statuses/user_timeline.json', user_timeline_query);
const userInfo = data[0].user;
if (!screen_name) {
screen_name = userInfo.screen_name;
}
const profileImageUrl = userInfo.profile_image_url || userInfo.profile_image_url_https;
ctx.state.data = {
title: `Twitter @${userInfo.name}`,
link: `https://twitter.com/${id}`,
link: `https://twitter.com/${screen_name}`,
image: profileImageUrl,
description: userInfo.description,
item: utils.ProcessFeed(ctx, {

View File

@ -10,6 +10,7 @@ const tokens = [
const graphQLEndpointsPlain = [
'/graphql/oUZZZ8Oddwxs8Cd3iW3UEA/UserByScreenName',
'/graphql/Lxg1V9AiIzzXEiP2c8dRnw/UserByRestId',
'/graphql/3XDB26fBve-MmjHaWTUZxA/TweetDetail',
'/graphql/QqZBEqganhHwmU9QscmIug/UserTweets',
'/graphql/wxoVeDnl0mP7VLhe6mTOdg/UserTweetsAndReplies',
@ -21,6 +22,7 @@ const graphQLEndpointsPlain = [
const graphQLMap = Object.fromEntries(graphQLEndpointsPlain.map((endpoint) => [endpoint.split('/')[3], endpoint]));
// captured from Twitter web
const featuresMap = {
UserByScreenName: JSON.stringify({
hidden_profile_likes_enabled: false,
@ -32,6 +34,15 @@ const featuresMap = {
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
}),
UserByRestId: JSON.stringify({
hidden_profile_likes_enabled: false,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
highlights_tweets_tab_ui_enabled: true,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
}),
UserTweets: JSON.stringify({
rweb_lists_timeline_redesign_enabled: true,
responsive_web_graphql_exclude_directive_enabled: true,

View File

@ -13,7 +13,7 @@ module.exports = async (ctx) => {
ctx.state.data = {
title: `Twitter @${userInfo.name}`,
link: `https://twitter.com/${id}/media`,
link: `https://twitter.com/${userInfo.screen_name}/media`,
image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'),
description: userInfo.description,
item: utils.ProcessFeed(ctx, {

View File

@ -17,7 +17,7 @@ module.exports = async (ctx) => {
ctx.state.data = {
title: `Twitter @${userInfo.name}`,
link: `https://twitter.com/${id}/status/${status}`,
link: `https://twitter.com/${userInfo.screen_name}/status/${status}`,
image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'),
description: userInfo.description,
item,

View File

@ -231,22 +231,36 @@ const userByScreenName = (screenName) =>
variables: `{"screen_name":"${screenName}","withHighlightedLabel":true}`,
features: featuresMap.UserByScreenName,
});
const getUserData = (cache, screenName) => cache.tryGet(`twitter-userdata-${screenName}`, () => userByScreenName(screenName));
const getUserID = async (cache, screenName) => (await getUserData(cache, screenName)).data.user.result.rest_id;
const getUser = async (cache, screenName) => (await getUserData(cache, screenName)).data.user.result.legacy;
const userByRestId = (restId) =>
twitterGot(`https://twitter.com/i/api${graphQLMap.UserByRestId}`, {
variables: `{"userId":"${restId}","withHighlightedLabel":true}`,
features: featuresMap.UserByRestId,
});
const userByAuto = (id) => {
if (id.startsWith('+')) {
return userByRestId(id.slice(1));
}
return userByScreenName(id);
};
const getUserData = (cache, id) => cache.tryGet(`twitter-userdata-${id}`, () => userByAuto(id));
const getUserID = async (cache, id) => (await getUserData(cache, id)).data.user.result.rest_id;
const getUser = async (cache, id) => (await getUserData(cache, id)).data.user.result.legacy;
const cacheTryGet = async (cache, screenName, params, func) => {
const id = await getUserID(cache, screenName);
const cacheTryGet = async (cache, _id, params, func) => {
const id = await getUserID(cache, _id);
if (id === undefined) {
throw Error('User not found');
}
const funcName = func.name;
const paramsString = JSON.stringify(params);
return cache.tryGet(`twitter:${id}:${funcName}:${paramsString}`, () => func(id, params), config.cache.routeExpire, false);
};
const getUserTweets = (cache, screenName, params = {}) => cacheTryGet(cache, screenName, params, getUserTweetsByID);
const getUserTweetsAndReplies = (cache, screenName, params = {}) => cacheTryGet(cache, screenName, params, getUserTweetsAndRepliesByID);
const getUserMedia = (cache, screenName, params = {}) => cacheTryGet(cache, screenName, params, getUserMediaByID);
const getUserLikes = (cache, screenName, params = {}) => cacheTryGet(cache, screenName, params, getUserLikesByID);
const getUserTweet = (cache, screenName, params) => cacheTryGet(cache, screenName, params, getUserTweetByStatus);
const getUserTweets = (cache, id, params = {}) => cacheTryGet(cache, id, params, getUserTweetsByID);
const getUserTweetsAndReplies = (cache, id, params = {}) => cacheTryGet(cache, id, params, getUserTweetsAndRepliesByID);
const getUserMedia = (cache, id, params = {}) => cacheTryGet(cache, id, params, getUserMediaByID);
const getUserLikes = (cache, id, params = {}) => cacheTryGet(cache, id, params, getUserLikesByID);
const getUserTweet = (cache, id, params) => cacheTryGet(cache, id, params, getUserTweetByStatus);
const getSearch = async (keywords, params = {}) => gatherLegacyFromLegacyApiData(await timelineKeywords(keywords, params), 'sq-I-t-');

View File

@ -23,7 +23,7 @@ module.exports = async (ctx) => {
ctx.state.data = {
title: `Twitter @${userInfo.name}`,
link: `https://twitter.com/${id}`,
link: `https://twitter.com/${userInfo.screen_name}`,
image: profileImageUrl.replace(/_normal.jpg$/, '.jpg'),
description: userInfo.description,
item: utils.ProcessFeed(ctx, {