mirror of
https://github.com/DIYgod/RSSHub.git
synced 2025-12-11 15:47:48 +08:00
@@ -495,6 +495,10 @@ RSSHub 提供下列 API 接口:
|
|||||||
|
|
||||||
<route name="用户" author="DIYgod" example="/twitter/user/DIYgod" path="/twitter/user/:id" :paramsDesc="['用户 twitter 名']"/>
|
<route name="用户" author="DIYgod" example="/twitter/user/DIYgod" path="/twitter/user/:id" :paramsDesc="['用户 twitter 名']"/>
|
||||||
|
|
||||||
|
<route name="列表" author="xyqfer" example="/twitter/list/ladyleet/javascript" path="/twitter/list/:id/:name" :paramsDesc="['用户 twitter 名', 'list 名称']"/>
|
||||||
|
|
||||||
|
> 官方文档: [How to use Twitter Lists](https://help.twitter.com/en/using-twitter/twitter-lists)
|
||||||
|
|
||||||
### Instagram
|
### Instagram
|
||||||
|
|
||||||
<route name="用户" author="DIYgod" example="/instagram/user/diygod" path="/instagram/user/:id" :paramsDesc="['用户 id']"/>
|
<route name="用户" author="DIYgod" example="/instagram/user/diygod" path="/instagram/user/:id" :paramsDesc="['用户 id']"/>
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ if (config.disqus && config.disqus.api_key) {
|
|||||||
// Twitter
|
// Twitter
|
||||||
if (config.twitter && config.twitter.consumer_key && config.twitter.consumer_secret && config.twitter.access_token && config.twitter.access_token_secret) {
|
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/user/:id', require('./routes/twitter/user'));
|
||||||
|
router.get('/twitter/list/:id/:name', require('./routes/twitter/list'));
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Twitter RSS is disabled for lacking config.');
|
logger.warn('Twitter RSS is disabled for lacking config.');
|
||||||
}
|
}
|
||||||
|
|||||||
23
routes/twitter/list.js
Normal file
23
routes/twitter/list.js
Normal file
@@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,124 +1,23 @@
|
|||||||
const Twit = require('twit');
|
const Twit = require('twit');
|
||||||
const URL = require('url');
|
|
||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
const T = new Twit(config.twitter);
|
const T = new Twit(config.twitter);
|
||||||
|
|
||||||
module.exports = async (ctx) => {
|
module.exports = async (ctx) => {
|
||||||
const id = ctx.params.id;
|
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 = `<br><video src="${video.url}" controls poster="${getOrigionImg(media.media_url_https)}" style="width: 100%"></video>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `<br><img referrerpolicy="no-referrer" src="${getOrigionImg(item.media_url_https)}">`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
img += content;
|
|
||||||
});
|
|
||||||
|
|
||||||
return img;
|
|
||||||
};
|
|
||||||
const formatUrl = (item) => {
|
|
||||||
let url = '';
|
|
||||||
item.entities.urls.forEach((u) => {
|
|
||||||
url += `<a href="${u.expanded_url}" target="_blank">${u.expanded_url}</a>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await T.get('statuses/user_timeline', {
|
const result = await T.get('statuses/user_timeline', {
|
||||||
screen_name: id,
|
screen_name: id,
|
||||||
tweet_mode: 'extended',
|
tweet_mode: 'extended',
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
|
|
||||||
ctx.state.data = {
|
ctx.state.data = {
|
||||||
title: `${data[0].user.name} 的 Twitter`,
|
title: `${data[0].user.name} 的 Twitter`,
|
||||||
link: `https://twitter.com/${id}/`,
|
link: `https://twitter.com/${id}/`,
|
||||||
description: data[0].user.description,
|
description: data[0].user.description,
|
||||||
item: data.map((item) => {
|
item: utils.ProcessFeed({
|
||||||
item = item.retweeted_status || item;
|
data,
|
||||||
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 += `<br><br>${author.name}: ${formatText(quoteData.full_text)}<br>`;
|
|
||||||
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}`,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
110
routes/twitter/utils.js
Normal file
110
routes/twitter/utils.js
Normal file
@@ -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 = `<br><video src="${video.url}" controls poster="${getOrigionImg(media.media_url_https)}" style="width: 100%"></video>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<br><img referrerpolicy="no-referrer" src="${getOrigionImg(item.media_url_https)}">`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
img += content;
|
||||||
|
});
|
||||||
|
|
||||||
|
return img;
|
||||||
|
};
|
||||||
|
const formatUrl = (item) => {
|
||||||
|
let url = '';
|
||||||
|
item.entities.urls.forEach((u) => {
|
||||||
|
url += `<a href="${u.expanded_url}" target="_blank">${u.expanded_url}</a>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 += `<br><br>${author.name}: ${formatText(quoteData.full_text)}<br>`;
|
||||||
|
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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user