diff --git a/docs/en/README.md b/docs/en/README.md
index 2b5475916..b82925c18 100644
--- a/docs/en/README.md
+++ b/docs/en/README.md
@@ -251,7 +251,19 @@ If no matching results were found, the server returns only a HTTP status code `2
#### User timeline
-
+
+
+### User following timeline
+
+
+
+::: warning
+
+User following timeline needs Twitter token corresponding id, so this route is only for self-hosted, see Install - Route-specific Configurations for details
+
+:::
+
+
#### List timeline
diff --git a/docs/en/install/README.md b/docs/en/install/README.md
index ec47aec53..3186d753e 100644
--- a/docs/en/install/README.md
+++ b/docs/en/install/README.md
@@ -318,6 +318,8 @@ When adding feeds using RSS readers with HTTP Basic Authentication support, auth
- `TWITTER_CONSUMER_SECRET`: Twitter Consumer Secret, support multiple keys, split them with `,`
+ - `TWITTER_TOKEN_{id}`: Twitter token corresponding id, replace `{id}` with id, the value is splitting consumer_key consumer_secret access_token access_token_secret with `,`, `{consumer_key},{consumer_secret},{access_token},{access_token_secret}`
+
- `youtube`: [API Key application](https://console.developers.google.com/)
- `YOUTUBE_KEY`: YouTube API Key
diff --git a/docs/install/README.md b/docs/install/README.md
index d0751a717..969346ea7 100644
--- a/docs/install/README.md
+++ b/docs/install/README.md
@@ -384,6 +384,8 @@ RSSHub 支持 `memory` 和 `redis` 两种缓存方式
- `TWITTER_CONSUMER_SECRET`: Twitter Consumer Secret,支持多个 key,用英文逗号 `,` 隔开,顺序与 key 对应
+ - `TWITTER_TOKEN_{id}`: 对应 id 的 Twitter token,`{id}` 替换为 id,值为 consumer_key consumer_secret access_token access_token_secret 用逗号隔开,即:`{consumer_key},{consumer_secret},{access_token},{access_token_secret}`
+
- youtube 全部路由: [申请地址](https://console.developers.google.com/)
- `YOUTUBE_KEY`: YouTube API Key
diff --git a/docs/social-media.md b/docs/social-media.md
index f40131945..b30d20a37 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -362,6 +362,18 @@ pageClass: routes
+### 用户关注时间线
+
+
+
+::: warning 注意
+
+用户关注时间线需要对应用户的 Twitter token, 所以只能自建,详情见部署页面的配置模块。
+
+:::
+
+
+
### 列表时间线
diff --git a/lib/config.js b/lib/config.js
index 4df35f9db..a3f8c9908 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,9 +1,13 @@
const bilibili_cookies = {};
+const twitter_tokens = {};
const envs = process.env;
for (const name in envs) {
if (name.startsWith('BILIBILI_COOKIE_')) {
const uid = name.slice(16);
bilibili_cookies[uid] = envs[name];
+ } else if (name.startsWith('TWITTER_TOKEN_')) {
+ const id = name.slice(14);
+ twitter_tokens[id] = envs[name];
}
}
@@ -42,6 +46,7 @@ module.exports = {
twitter: {
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
+ tokens: twitter_tokens,
},
youtube: {
key: process.env.YOUTUBE_KEY,
diff --git a/lib/router.js b/lib/router.js
index 2ac65f6e6..ce4c00b5f 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -180,6 +180,7 @@ if (config.twitter && config.twitter.consumer_key && config.twitter.consumer_sec
} else {
logger.warn('Twitter RSS is disabled due to the lack of token, api key or relevant config in config.js.');
}
+router.get('/twitter/followings/:id', require('./routes/twitter/followings'));
// Instagram
router.get('/instagram/user/:id', require('./routes/instagram/user'));
diff --git a/lib/routes/twitter/followings.js b/lib/routes/twitter/followings.js
new file mode 100644
index 000000000..719d60c86
--- /dev/null
+++ b/lib/routes/twitter/followings.js
@@ -0,0 +1,37 @@
+const utils = require('./utils');
+const config = require('@/config');
+const T = {};
+const Twit = require('twit');
+
+module.exports = async (ctx) => {
+ const id = ctx.params.id;
+ const cookie = config.twitter.tokens[id];
+ if (!cookie) {
+ throw Error(`lacking twitter token for user ${id}`);
+ } else if (!T[id]) {
+ const token = cookie.split(',');
+ T[id] = new Twit({
+ consumer_key: token[0],
+ consumer_secret: token[1],
+ access_token: token[2],
+ access_token_secret: token[3],
+ });
+ }
+
+ const result = await T[id].get('statuses/home_timeline', {
+ tweet_mode: 'extended',
+ count: 100,
+ });
+ const data = result.data;
+
+ ctx.state.data = {
+ title: `${id} 的 Twitter 关注时间线`,
+ link: `https://twitter.com/${id}/`,
+ item: utils.ProcessFeed(
+ {
+ data,
+ },
+ true
+ ),
+ };
+};
diff --git a/lib/routes/twitter/utils.js b/lib/routes/twitter/utils.js
index 8fa43e92c..74c54e75e 100644
--- a/lib/routes/twitter/utils.js
+++ b/lib/routes/twitter/utils.js
@@ -2,7 +2,7 @@ const URL = require('url');
const config = require('@/config');
const Twit = require('twit');
-const ProcessFeed = ({ data = [] }) => {
+const ProcessFeed = ({ data = [] }, showAuthor = false) => {
const getQueryParams = (url) => URL.parse(url, true).query;
const getOrigionImg = (url) => {
// https://greasyfork.org/zh-CN/scripts/2312-resize-image-on-open-image-in-new-tab/code#n150
@@ -100,8 +100,10 @@ const ProcessFeed = ({ data = [] }) => {
}
return {
- title: `${item.in_reply_to_screen_name ? 'Re ' : ''}${formatText(item.full_text)}`,
- description: `${item.in_reply_to_screen_name ? 'Re ' : ''}${formatText(item.full_text)}${url}${img}${quote}`,
+ title: `${showAuthor ? item.user.name + ': ' : ''}${item.in_reply_to_screen_name ? 'Re ' : ''}${formatText(item.full_text)}`,
+ description: `${showAuthor ? `
${item.user.name}:
` : ''}${
+ item.in_reply_to_screen_name ? 'Re ' : ''
+ }${formatText(item.full_text)}${url}${img}${quote}`,
pubDate: new Date(item.created_at).toUTCString(),
link: `https://twitter.com/${item.user.screen_name}/status/${item.id_str}`,
};