feat: add mastodon user timeline (#5188)

This commit is contained in:
notofoe 2020-07-29 21:27:23 +08:00 committed by GitHub
parent 580fea513f
commit e7d62ebcd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 224 additions and 38 deletions

View File

@ -427,6 +427,11 @@ See the relation between access key/code and white/blacklisting.
- `DISCUZ_COOKIE_{cid}`: Cookie of a forum powered by discuz, cid can be anything from 00 to 99. When visiting route discuz, using cid to specify this cookie.
- Mastodon user timeline: apply api here `https://mastodon.example/settings/applications`, please check scope `read:search`
- `MASTODON_API_HOST`: api instance domain
- `MASTODON_API_ACCESS_TOKEN`: user access token
- `MASTODON_API_ACCT_DOMAIN`: acct domain for particular instance
- Sci-hub for scientific journal routes:
- `SCIHUB_HOST`: The Sci-hub mirror address that is accssible from your location, default to `https://sci-hub.tw`.

View File

@ -30,15 +30,26 @@ pageClass: routes
::: tip
Offical user RSS: https://**:instance**/users/**:username**.atom or https://**:instance**/users/**:username**.rss
Official user RSS:
- RSS: `https://**:instance**/users/**:username**.rss` ([Example](https://pawoo.net/users/pawoo_support.rss))
- Atom: ~~`https://**:instance**/users/**:username**.atom`~~ (Only for pawoo.net, [example](https://pawoo.net/users/pawoo_support.atom))
For example, https://pawoo.net/users/pawoo_support.atom or https://pawoo.net/users/pawoo_support.rss
These feed do not include boosts (a.k.a. reblogs). RSSHub provides a feed for user timeline based on the Mastodon API, but to use that, you will need to create application on a Mastodon instance, and configure your RSSHub instance. Check the [Deploy Guide](/en/install/#route-specific-configurations) for route-specific configurations.
:::
### User timeline
<RouteEn author="notofoe" example="/mastodon/acct/CatWhitney@mastodon.social" path="/mastodon/acct/:acct/:only_media?" :paramsDesc="['Webfinger account URI', 'whether only display media content, default to false, any value to true']"/>
### Timeline
<RouteEn author="hoilc" example="/mastodon/timeline/pawoo.net/true" path="/mastodon/timeline/:site/:only_media?" :paramsDesc="['instance address, noly domain, no `http://` or `https://` protocol header', 'whether only display media content, default to false, any value to true']"/>
<RouteEn author="hoilc" example="/mastodon/timeline/pawoo.net/true" path="/mastodon/timeline/:site/:only_media?" :paramsDesc="['instance address, only domain, no `http://` or `https://` protocol header', 'whether only display media content, default to false, any value to true']"/>
### User timeline (backup)
<RouteEn author="notofoe" example="/mastodon/account_id/mastodon.social/23634/statuses/only_media" path="/mastodon/account/:site/:account_id/statuses/:only_media?" :paramsDesc="['instance address, only domain, no `http://` or `https://` protocol header', 'account id. login your instance, then search for the user profile; the account id is in the url', 'whether only display media content, default to false, any value to true']"/>
## piapro

View File

@ -463,6 +463,11 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行
- `WEIBO_APP_SECRET`: 微博 App Secret
- `WEIBO_REDIRECT_URL`: 微博登录授权回调地址,默认为 `RSSHub 地址/weibo/timeline/0`,自定义回调地址请确保最后可以转跳到 `RSSHub 地址/weibo/timeline/0?code=xxx`
- Mastodon 用户时间线路由:访问 `https://mastodon.example/settings/applications` 申请(替换掉 `mastodon.example`)。需要 `read:search` 权限
- `MASTODON_API_HOST`: API 请求的实例
- `MASTODON_API_ACCESS_TOKEN`: 用户 access token, 申请应用后,在应用配置页可以看到申请者的 access token
- `MASTODON_API_ACCT_DOMAIN`: 该实例本地用户 acct 标识的域名
- 饭否 全部路由:[申请地址](https://github.com/FanfouAPI/FanFouAPIDoc/wiki/Oauth)
- `FANFOU_CONSUMER_KEY`: 饭否 Consumer Key

View File

@ -322,16 +322,29 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
::: tip 提示
官方提供了用户时间线 RSS: `https://**:instance**/users/**:username**.atom``https://**:instance**/users/**:username**.rss`
通常来说,各实例提供用户时间线的订阅源,如下:
例如:<https://pawoo.net/users/pawoo_support.atom><https://pawoo.net/users/pawoo_support.rss>
- RSS: `https://**:instance**/users/**:username**.rss`
- Atom: ~~`https://**:instance**/users/**:username**.atom`~~ (仅 pawoo.net)
例如:<https://pawoo.net/users/pawoo_support.rss><https://pawoo.net/users/pawoo_support.atom>
上述订阅源的内容不包括用户的转嘟。RSSHub 提供基于 Mastodon API 的订阅源,但需要您在某个 Mastodon 实例申请 API并对 RSSHub 实例进行配置。详情见部署页面的配置模块。
:::
### 用户公共时间线
<Route author="notofoe" example="/mastodon/acct/CatWhitney@mastodon.social/statuses" path="/mastodon/acct/:acct/statuses/:only_media?" :paramsDesc="['Webfinger account URI, 形如 `user@host`', '是否只显示包含媒体(图片或视频)的推文, 默认置空为否, 任意值为是']"/>
### 实例公共时间线
<Route author="hoilc" example="/mastodon/timeline/pawoo.net/true" path="/mastodon/timeline/:site/:only_media?" :paramsDesc="['实例地址, 仅域名, 不包括`http://`或`https://`协议头', '是否只显示包含媒体(图片或视频)的推文, 默认置空为否, 任意值为是']"/>
### 用户公共时间线(备用)
<Route author="notofoe" example="/mastodon/account_id/mastodon.social/23634/statuses/only_media" path="/mastodon/account/:site/:account_id/statuses/:only_media?" :paramsDesc="['实例地址, 仅域名, 不包括`http://`或`https://`协议头', '用户 ID. 登录实例后, 搜索用户并进入用户页, 在地址中可以找到这串数字', '是否只显示包含媒体(图片或视频)的推文, 默认置空为否, 任意值为是']"/>
## piapro
### 用户最新作品

View File

@ -144,6 +144,11 @@ const calculateValue = () => {
host: envs.BTBYR_HOST || 'https://bt.byr.cn/',
cookies: envs.BTBYR_COOKIE,
},
mastodon: {
apiHost: envs.MASTODON_API_HOST,
accessToken: envs.MASTODON_API_ACCESS_TOKEN,
acctDomain: envs.MASTODON_API_ACCT_DOMAIN,
},
xiaoyuzhou: {
device_id: envs.XIAOYUZHOU_ID,
refresh_token: envs.XIAOYUZHOU_TOKEN,

View File

@ -2285,6 +2285,8 @@ router.get('/ikea/uk/offer', require('./routes/ikea/uk/offer'));
// Mastodon
router.get('/mastodon/timeline/:site/:only_media?', require('./routes/mastodon/timeline'));
router.get('/mastodon/account_id/:site/:account_id/statuses/:only_media?', require('./routes/mastodon/account_id'));
router.get('/mastodon/acct/:acct/statuses/:only_media?', require('./routes/mastodon/acct'));
// Kernel Aliyun
router.get('/aliyun-kernel/index', require('./routes/aliyun-kernel/index'));

View File

@ -0,0 +1,39 @@
const got = require('@/utils/got');
const utils = require('./utils');
module.exports = async (ctx) => {
const site = ctx.params.site;
const account_id = ctx.params.account_id;
const only_media = ctx.params.only_media ? true : false;
const statuses_url = `http://${site}/api/v1/accounts/${account_id}/statuses`;
const statuses_response = await got({
method: 'get',
url: statuses_url,
});
let data = statuses_response.data;
let account_data;
if (data.length !== 0 && data[0].account !== null) {
account_data = data[0].account;
} else {
const account_url = `http://${site}/api/v1/accounts/${account_id}`;
const account_response = await got({
method: 'get',
url: account_url,
});
account_data = account_response.data;
}
if (only_media === true) {
data = data.filter((item) => item.media_attachments.length !== 0);
}
ctx.state.data = {
title: `${account_data.display_name} (@${account_data.acct})`,
link: `${account_data.url}`,
description: `${account_data.note}`,
item: utils.ProcessFeed(data),
allowEmpty: true,
};
};

View File

@ -0,0 +1,87 @@
const got = require('@/utils/got');
const utils = require('./utils');
const config = require('@/config').value;
const mastodonConfig = config.mastodon;
module.exports = async (ctx) => {
const getAccountIdByAcct = async (acct) => {
if (!(mastodonConfig.apiHost && mastodonConfig.accessToken && mastodonConfig.acctDomain)) {
throw 'this route require api configuration, please check <a href="https://docs.rsshub.app/en/install/#configuration">documentation</a>';
}
const site = mastodonConfig.apiHost;
const search_url = `http://${site}/api/v2/search`;
const cacheUid = `mastodon_acct_id/${site}/${acct}`;
return await ctx.cache.tryGet(cacheUid, async () => {
const search_response = await got({
method: 'get',
url: search_url,
headers: {
Authorization: `Bearer ${mastodonConfig.accessToken}`,
},
searchParams: {
q: acct,
type: 'accounts',
},
});
const [acctUser, acctHost] = acct.split('@').filter((e) => e);
let acctOnServer;
if (acctHost) {
if (acctHost === mastodonConfig.acctDomain) {
acctOnServer = acctUser;
} else {
acctOnServer = acctUser + '@' + acctHost;
}
} else {
acctOnServer = acctUser;
}
const accountData = search_response.data.accounts.filter((item) => item.acct === acctOnServer);
if (accountData.length === 0) {
throw `acct ${acct} not found`;
}
return accountData[0].id;
});
};
const acct = ctx.params.acct;
const only_media = ctx.params.only_media ? true : false;
const site = mastodonConfig.apiHost;
const account_id = await getAccountIdByAcct(acct);
const statuses_url = `http://${site}/api/v1/accounts/${account_id}/statuses`;
const statuses_response = await got({
method: 'get',
url: statuses_url,
});
let data = statuses_response.data;
let account_data;
if (data.length !== 0 && data[0].account !== null) {
account_data = data[0].account;
} else {
const account_url = `http://${site}/api/v1/accounts/${account_id}`;
const account_response = await got({
method: 'get',
url: account_url,
});
account_data = account_response.data;
}
if (only_media === true) {
data = data.filter((item) => item.media_attachments.length !== 0);
}
ctx.state.data = {
title: `${account_data.display_name} (@${account_data.acct})`,
link: `${account_data.url}`,
description: `${account_data.note}`,
item: utils.ProcessFeed(data),
allowEmpty: true,
};
};

View File

@ -1,4 +1,5 @@
const got = require('@/utils/got');
const utils = require('./utils');
module.exports = async (ctx) => {
const site = ctx.params.site;
@ -9,41 +10,9 @@ module.exports = async (ctx) => {
const response = await got.get(url);
const list = response.data;
const ProcessFeed = (data) =>
data.map((item) => {
const title = item.content
.replace(/<span.*?>|<\/span.*?>/gm, '')
.replace(/<(?:.|\n)*?>/gm, '\n')
.split('\n')
.filter((s) => s !== '')[0];
const media = item.media_attachments
.map((item) => {
switch (item.type) {
case 'gifv':
return `<br><video src="${item.url}" autoplay loop>GIF</video>`;
case 'video':
return `<br><video src="${item.url}" controls loop>Video</video>`;
case 'image':
return `<br><img src="${item.url}">`;
default:
return '';
}
})
.join('');
return {
title: title,
author: item.account.display_name,
description: item.content + media,
pubDate: new Date(item.created_at).toUTCString(),
link: item.uri,
};
});
ctx.state.data = {
title: `Local Public${ctx.params.only_media ? ' Media' : ''} Timeline on ${site}`,
link: `http://${site}`,
item: ProcessFeed(list),
item: utils.ProcessFeed(list),
};
};

View File

@ -0,0 +1,50 @@
const ProcessFeed = (data) =>
data.map((item) => {
const content = item.content
? item.content
.replace(/<span.*?>|<\/span.*?>/gm, '')
.replace(/<(?:.|\n)*?>/gm, '\n')
.split('\n')
.filter((s) => s !== '')[0]
: '';
const media = item.media_attachments
.map((item) => {
switch (item.type) {
case 'gifv':
return `<br><video src="${item.url}" autoplay loop>GIF</video>`;
case 'video':
return `<br><video src="${item.url}" controls loop>Video</video>`;
case 'image':
return `<br><img src="${item.url}">`;
default:
return '';
}
})
.join('');
let author, link, titleAuthor;
if (item.reblog !== null) {
author = `${item.reblog.account.display_name} (@${item.reblog.account.acct})`;
link = item.reblog.url;
titleAuthor = `Re @${item.reblog.account.username}`;
} else {
author = `${item.account.display_name} (@${item.account.acct})`;
link = item.url;
titleAuthor = `@${item.account.username}`;
}
const titleText = item.sentitive === true ? `(CW) ${item.spoiler_text}` : content;
return {
title: `${titleAuthor}: "${titleText}"`,
author: author,
description: content + media,
pubDate: new Date(item.created_at).toUTCString(),
link: link,
guid: item.uri,
};
});
module.exports = {
ProcessFeed: ProcessFeed,
};