diff --git a/docs/README.md b/docs/README.md
index 9ba80cfb09..fdbc1555d6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -495,6 +495,10 @@ RSSHub 提供下列 API 接口:
+
+
+> 官方文档: [How to use Twitter Lists](https://help.twitter.com/en/using-twitter/twitter-lists)
+
### Instagram
diff --git a/router.js b/router.js
index 306c60fa03..c0c68fa69e 100644
--- a/router.js
+++ b/router.js
@@ -236,6 +236,7 @@ if (config.disqus && config.disqus.api_key) {
// Twitter
if (config.twitter && config.twitter.consumer_key && config.twitter.consumer_secret && config.twitter.access_token && config.twitter.access_token_secret) {
router.get('/twitter/user/:id', require('./routes/twitter/user'));
+ router.get('/twitter/list/:id/:name', require('./routes/twitter/list'));
} else {
logger.warn('Twitter RSS is disabled for lacking config.');
}
diff --git a/routes/twitter/list.js b/routes/twitter/list.js
new file mode 100644
index 0000000000..00990ddbef
--- /dev/null
+++ b/routes/twitter/list.js
@@ -0,0 +1,23 @@
+const Twit = require('twit');
+const config = require('../../config');
+const utils = require('./utils');
+
+const T = new Twit(config.twitter);
+
+module.exports = async (ctx) => {
+ const { id, name } = ctx.params;
+ const result = await T.get('lists/statuses', {
+ owner_screen_name: id,
+ slug: name,
+ tweet_mode: 'extended',
+ });
+ const data = result.data;
+
+ ctx.state.data = {
+ title: `Twitter List - ${id}/${name}`,
+ link: `https://twitter.com/${id}/lists/${name}`,
+ item: utils.ProcessFeed({
+ data,
+ }),
+ };
+};
diff --git a/routes/twitter/user.js b/routes/twitter/user.js
index 3a9e7f12c5..b5bff857d5 100644
--- a/routes/twitter/user.js
+++ b/routes/twitter/user.js
@@ -1,124 +1,23 @@
const Twit = require('twit');
-const URL = require('url');
const config = require('../../config');
+const utils = require('./utils');
const T = new Twit(config.twitter);
module.exports = async (ctx) => {
const id = ctx.params.id;
-
- 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
- let m = null;
- if ((m = url.match(/^(https?:\/\/\w+\.twimg\.com\/media\/[^/:]+)\.(jpg|jpeg|gif|png|bmp|webp)(:\w+)?$/i))) {
- let format = m[2];
- if (m[2] === 'jpeg') {
- format = 'jpg';
- }
- return `${m[1]}?format=${format}&name=orig`;
- } else if ((m = url.match(/^(https?:\/\/\w+\.twimg\.com\/.+)(\?.+)$/i))) {
- const pars = getQueryParams(url);
- if (!pars.format || !pars.name) {
- return url;
- }
- if (pars.name === 'orig') {
- return url;
- }
- return m[1] + '?format=' + pars.format + '&name=orig';
- } else {
- return url;
- }
- };
-
- const formatText = (text) => text.replace(/https:\/\/t\.co(.*)/g, '');
- const formatVideo = (media) => {
- let content = '';
- const video = media.video_info.variants.reduce((video, item) => {
- if ((item.bitrate || 0) > (video.bitrate || 0)) {
- video = item;
- }
- return video;
- }, {});
-
- if (video.url) {
- content = `
`;
- }
-
- return content;
- };
-
- const formatMedia = (item) => {
- let img = '';
- item.extended_entities &&
- item.extended_entities.media.forEach((item) => {
- // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/extended-entities-object
- let content = '';
- switch (item.type) {
- case 'animated_gif':
- case 'video':
- content = formatVideo(item);
- break;
-
- case 'photo':
- default:
- content = `
`;
- break;
- }
-
- img += content;
- });
-
- return img;
- };
- const formatUrl = (item) => {
- let url = '';
- item.entities.urls.forEach((u) => {
- url += `${u.expanded_url}`;
- });
-
- return url;
- };
-
const result = await T.get('statuses/user_timeline', {
screen_name: id,
tweet_mode: 'extended',
});
-
const data = result.data;
ctx.state.data = {
title: `${data[0].user.name} 的 Twitter`,
link: `https://twitter.com/${id}/`,
description: data[0].user.description,
- item: data.map((item) => {
- item = item.retweeted_status || item;
- item.full_text = formatText(item.full_text);
- const img = formatMedia(item);
- let url = '';
- let quote = '';
-
- if (item.is_quote_status) {
- const quoteData = item.quoted_status;
-
- if (quoteData) {
- const author = quoteData.user;
- quote += `
${author.name}: ${formatText(quoteData.full_text)}
`;
- quote += formatMedia(quoteData);
- quote += formatUrl(quoteData);
- } else {
- url = formatUrl(item);
- }
- } else {
- url = formatUrl(item);
- }
-
- return {
- title: `${item.in_reply_to_screen_name ? 'Re ' : ''}${item.full_text}`,
- description: `${item.in_reply_to_screen_name ? 'Re ' : ''}${item.full_text}${url}${img}${quote}`,
- pubDate: new Date(item.created_at).toUTCString(),
- link: `https://twitter.com/${id}/status/${item.id_str}`,
- };
+ item: utils.ProcessFeed({
+ data,
}),
};
};
diff --git a/routes/twitter/utils.js b/routes/twitter/utils.js
new file mode 100644
index 0000000000..b3cadaa694
--- /dev/null
+++ b/routes/twitter/utils.js
@@ -0,0 +1,110 @@
+const URL = require('url');
+
+const ProcessFeed = ({ data = [] }) => {
+ 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
+ let m = null;
+ if ((m = url.match(/^(https?:\/\/\w+\.twimg\.com\/media\/[^/:]+)\.(jpg|jpeg|gif|png|bmp|webp)(:\w+)?$/i))) {
+ let format = m[2];
+ if (m[2] === 'jpeg') {
+ format = 'jpg';
+ }
+ return `${m[1]}?format=${format}&name=orig`;
+ } else if ((m = url.match(/^(https?:\/\/\w+\.twimg\.com\/.+)(\?.+)$/i))) {
+ const pars = getQueryParams(url);
+ if (!pars.format || !pars.name) {
+ return url;
+ }
+ if (pars.name === 'orig') {
+ return url;
+ }
+ return m[1] + '?format=' + pars.format + '&name=orig';
+ } else {
+ return url;
+ }
+ };
+
+ const formatText = (text) => text.replace(/https:\/\/t\.co(.*)/g, '');
+ const formatVideo = (media) => {
+ let content = '';
+ const video = media.video_info.variants.reduce((video, item) => {
+ if ((item.bitrate || 0) > (video.bitrate || 0)) {
+ video = item;
+ }
+ return video;
+ }, {});
+
+ if (video.url) {
+ content = `
`;
+ }
+
+ return content;
+ };
+
+ const formatMedia = (item) => {
+ let img = '';
+ item.extended_entities &&
+ item.extended_entities.media.forEach((item) => {
+ // https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/extended-entities-object
+ let content = '';
+ switch (item.type) {
+ case 'animated_gif':
+ case 'video':
+ content = formatVideo(item);
+ break;
+
+ case 'photo':
+ default:
+ content = `
`;
+ break;
+ }
+
+ img += content;
+ });
+
+ return img;
+ };
+ const formatUrl = (item) => {
+ let url = '';
+ item.entities.urls.forEach((u) => {
+ url += `${u.expanded_url}`;
+ });
+
+ return url;
+ };
+
+ return data.map((item) => {
+ item = item.retweeted_status || item;
+ item.full_text = formatText(item.full_text);
+ const img = formatMedia(item);
+ let url = '';
+ let quote = '';
+
+ if (item.is_quote_status) {
+ const quoteData = item.quoted_status;
+
+ if (quoteData) {
+ const author = quoteData.user;
+ quote += `
${author.name}: ${formatText(quoteData.full_text)}
`;
+ quote += formatMedia(quoteData);
+ quote += formatUrl(quoteData);
+ } else {
+ url = formatUrl(item);
+ }
+ } else {
+ url = formatUrl(item);
+ }
+
+ return {
+ title: `${item.in_reply_to_screen_name ? 'Re ' : ''}${item.full_text}`,
+ description: `${item.in_reply_to_screen_name ? 'Re ' : ''}${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}`,
+ };
+ });
+};
+
+module.exports = {
+ ProcessFeed,
+};