diff --git a/docs/en/social-media.md b/docs/en/social-media.md index a95eafe0c..0cd5ed040 100644 --- a/docs/en/social-media.md +++ b/docs/en/social-media.md @@ -494,15 +494,15 @@ generates ### User timeline - + ### User media - + ### User following timeline - + ::: warning @@ -514,11 +514,11 @@ This route requires Twitter token's corresponding id, therefore it's only availa ### List timeline - + ### User likes - + ### Keyword @@ -530,7 +530,7 @@ This route requires Twitter token's corresponding id, therefore it's only availa ### Collection - + ::: warning @@ -542,7 +542,7 @@ This route requires Twitter token's corresponding id, therefore it's only availa ### Tweet Details - + ## Vimeo diff --git a/docs/social-media.md b/docs/social-media.md index 504134dde..011799ea0 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -832,11 +832,11 @@ Instagram Stories 没有可靠的 guid,你的 RSS 阅读器可能将同一条 ### 用户时间线 - + ### 用户媒体时间线 - + ### 用户关注时间线 @@ -880,7 +880,7 @@ Instagram Stories 没有可靠的 guid,你的 RSS 阅读器可能将同一条 ### 推文详情 - + ## Vimeo diff --git a/lib/v2/twitter/developer-api/user.js b/lib/v2/twitter/developer-api/user.js index 4ec54d0cc..b7e0eb613 100644 --- a/lib/v2/twitter/developer-api/user.js +++ b/lib/v2/twitter/developer-api/user.js @@ -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, { diff --git a/lib/v2/twitter/web-api/constants.js b/lib/v2/twitter/web-api/constants.js index 8f9aa3020..fe18ac2aa 100644 --- a/lib/v2/twitter/web-api/constants.js +++ b/lib/v2/twitter/web-api/constants.js @@ -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, diff --git a/lib/v2/twitter/web-api/media.js b/lib/v2/twitter/web-api/media.js index 7944834d0..53f03b98e 100644 --- a/lib/v2/twitter/web-api/media.js +++ b/lib/v2/twitter/web-api/media.js @@ -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, { diff --git a/lib/v2/twitter/web-api/tweet.js b/lib/v2/twitter/web-api/tweet.js index 76d2c04e3..a53c09dcb 100644 --- a/lib/v2/twitter/web-api/tweet.js +++ b/lib/v2/twitter/web-api/tweet.js @@ -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, diff --git a/lib/v2/twitter/web-api/twitter-api.js b/lib/v2/twitter/web-api/twitter-api.js index 64408334b..e3ad4a9a8 100644 --- a/lib/v2/twitter/web-api/twitter-api.js +++ b/lib/v2/twitter/web-api/twitter-api.js @@ -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-'); diff --git a/lib/v2/twitter/web-api/user.js b/lib/v2/twitter/web-api/user.js index 5eb170834..100fcbf9b 100644 --- a/lib/v2/twitter/web-api/user.js +++ b/lib/v2/twitter/web-api/user.js @@ -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, {