diff --git a/docs/social-media.md b/docs/social-media.md
index 9ffdadd889..893885f2ef 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -206,6 +206,16 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
+### 用户关注动态
+
+
+::: warning 注意
+
+用户动态需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
+
+:::
+
+
### 用户关注视频动态
diff --git a/lib/router.js b/lib/router.js
index b5a7edba52..8c6ef2c56b 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -85,6 +85,7 @@ router.get('/bilibili/followings/article/:uid', require('./routes/bilibili/follo
router.get('/bilibili/readlist/:listid', require('./routes/bilibili/readlist'));
router.get('/bilibili/weekly', require('./routes/bilibili/weekly_recommend'));
router.get('/bilibili/manga/update/:comicid', require('./routes/bilibili/manga_update'));
+router.get('/bilibili/followings/dynamic/:uid/:disableEmbed?', require('./routes/bilibili/followings_dynamic'));
// bangumi
router.get('/bangumi/calendar/today', require('./routes/bangumi/calendar/today'));
diff --git a/lib/routes/bilibili/followings_dynamic.js b/lib/routes/bilibili/followings_dynamic.js
new file mode 100644
index 0000000000..af83923b97
--- /dev/null
+++ b/lib/routes/bilibili/followings_dynamic.js
@@ -0,0 +1,127 @@
+const got = require('@/utils/got');
+const cache = require('./cache');
+const config = require('@/config').value;
+const utils = require('./utils');
+const JSONbig = require('json-bigint');
+
+module.exports = async (ctx) => {
+ const uid = String(ctx.params.uid);
+ const disableEmbed = ctx.params.disableEmbed;
+ const name = await cache.getUsernameFromUID(ctx, uid);
+
+ const cookie = config.bilibili.cookies[uid];
+ if (cookie === undefined) {
+ throw Error('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
+ }
+
+ const response = await got({
+ method: 'get',
+ url: `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${uid}&type_list=268435455`,
+ headers: {
+ Referer: `https://space.bilibili.com/${uid}/`,
+ Cookie: cookie,
+ },
+ });
+ if (response.data.code === -6) {
+ throw Error('对应 uid 的 Bilibili 用户的 Cookie 已过期');
+ }
+ const data = JSONbig.parse(response.body).data.cards;
+
+ ctx.state.data = {
+ title: `${name} 关注的动态`,
+ link: `https://t.bilibili.com`,
+ description: `${name} 关注的动态`,
+ item: data.map((item) => {
+ const parsed = JSONbig.parse(item.card);
+ const data = parsed.item || parsed;
+ const origin = parsed.origin ? JSONbig.parse(parsed.origin) : null;
+
+ // img
+ let imgHTML = '';
+ const getImgs = (data) => {
+ let imgs = '';
+ // 动态图片
+ if (data.pictures) {
+ for (let i = 0; i < data.pictures.length; i++) {
+ imgs += `
`;
+ }
+ }
+ // 专栏封面
+ if (data.image_urls) {
+ for (let i = 0; i < data.image_urls.length; i++) {
+ imgs += `
`;
+ }
+ }
+ // 视频封面
+ if (data.pic) {
+ imgs += `
`;
+ }
+ // 音频/番剧/直播间封面/小视频封面
+ if (data.cover && data.cover.unclipped) {
+ imgs += `
`;
+ } else if (data.cover) {
+ imgs += `
`;
+ }
+ // 专题页封面
+ if (data.sketch && data.sketch.cover_url) {
+ imgs += `
`;
+ }
+ return imgs;
+ };
+
+ imgHTML += getImgs(data);
+
+ if (origin) {
+ imgHTML += getImgs(origin.item || origin);
+ }
+ // video小视频
+ let videoHTML = '';
+ if (data.video_playurl) {
+ videoHTML += ``;
+ }
+ // some rss readers disallow http content.
+ // 部分 RSS 阅读器要求内容必须使用https传输
+ // bilibili short video does support https request, but https request may timeout ocassionally.
+ // to maximize content availability, here add two source tags.
+ // bilibili的API中返回的视频地址采用http,然而经验证,短视频地址支持https访问,但偶尔会返回超时错误(可能是网络原因)。
+ // 因此保险起见加入两个source标签
+ // link
+ let link = '';
+ if (data.dynamic_id) {
+ link = `https://t.bilibili.com/${data.dynamic_id}`;
+ } else if (item.desc && item.desc.dynamic_id) {
+ link = `https://t.bilibili.com/${item.desc.dynamic_id}`;
+ }
+
+ const getTitle = (data) => data.title || data.description || data.content || (data.vest && data.vest.content) || '';
+ const getDes = (data) =>
+ data.dynamic || data.desc || data.description || data.content || data.summary || (data.vest && data.vest.content) + (data.sketch && `
${data.sketch.title}
${data.sketch.desc_text}`) || data.intro || '';
+ const getOriginDes = (data) => (data && (data.apiSeasonInfo && data.apiSeasonInfo.title && `//转发自: ${data.apiSeasonInfo.title}`) + (data.index_title && `
${data.index_title}`)) || '';
+ const getOriginName = (data) => data.uname || (data.author && data.author.name) || data.upper || (data.user && (data.user.uname || data.user.name)) || (data.owner && data.owner.name) || '';
+ const getOriginTitle = (data) => (data.title ? `${data.title}
` : '');
+ const getIframe = (data) => (!disableEmbed && data && data.aid ? `
${utils.iframe(data.aid)}
` : '');
+
+ // emoji
+ let data_content = getDes(data);
+ if (item.display.emoji_info) {
+ const emoji = item.display.emoji_info.emoji_details;
+ emoji.forEach(function (item) {
+ data_content = data_content.replace(
+ new RegExp(`\\${item.text}`, 'g'),
+ `
`
+ );
+ });
+ }
+
+ return {
+ title: getTitle(data),
+ author: item.desc.user_profile.info.uname,
+ description: `${data_content || getDes(data)}${
+ origin && getOriginName(origin) ? `
//转发自: @${getOriginName(origin)}: ${getOriginTitle(origin.item || origin)}${getDes(origin.item || origin)}` : `${getOriginDes(origin)}`
+ }${getIframe(data)}${getIframe(origin)}${imgHTML ? `
${imgHTML}` : ''}${videoHTML ? `
${videoHTML}` : ''}`,
+ pubDate: new Date(item.desc.timestamp * 1000).toUTCString(),
+ link: link,
+ };
+ }),
+ };
+};