feat(bilibili): add messages routes (#20890)
* feat(bilibili): add messages routes * fix(bilibili/message): do not display summary when no unread messages * fix(bilibili/message): update cache duration to use configurable routeExpire
This commit is contained in:
parent
c21c82fc05
commit
8bb3d6f019
|
|
@ -0,0 +1,146 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/at/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/at/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '@我的',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface AtItem {
|
||||
id: number;
|
||||
user: {
|
||||
mid: number;
|
||||
fans: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
mid_link: string;
|
||||
follow: boolean;
|
||||
};
|
||||
item: {
|
||||
subject_id: number;
|
||||
root_id: number;
|
||||
source_id: number;
|
||||
target_id: number;
|
||||
type: string;
|
||||
business_id: number;
|
||||
business: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
image: string;
|
||||
uri: string;
|
||||
native_uri: string;
|
||||
detail_title: string;
|
||||
source_content: string;
|
||||
at_details: unknown[];
|
||||
};
|
||||
at_time: number;
|
||||
}
|
||||
|
||||
interface AtResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
cursor: {
|
||||
is_end: boolean;
|
||||
id: number;
|
||||
time: number;
|
||||
};
|
||||
items: AtItem[];
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await cache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
const response = await ofetch<AtResponse>('https://api.bilibili.com/x/msgfeed/at', {
|
||||
query: {
|
||||
platform: 'web',
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message ?? `Error code ${response.code}`);
|
||||
}
|
||||
|
||||
const items: DataItem[] = (response.data.items || []).map((item) => {
|
||||
const atUser = item.user;
|
||||
const atItem = item.item;
|
||||
const sourceContent = atItem.source_content;
|
||||
|
||||
let description = `<p><strong>${atUser.nickname}</strong> @了你:</p>`;
|
||||
description += `<blockquote>${sourceContent}</blockquote>`;
|
||||
|
||||
if (atItem.image) {
|
||||
description += `<p><img src="${atItem.image.replace('http://', 'https://')}" /></p>`;
|
||||
}
|
||||
|
||||
description += `<p>来自:${atItem.business} - ${atItem.title}</p>`;
|
||||
|
||||
// Generate link with root_id for direct navigation
|
||||
let link = atItem.uri;
|
||||
if (atItem.root_id && atItem.uri) {
|
||||
link = `${atItem.uri}/#reply${atItem.root_id}`;
|
||||
} else if (atItem.source_id && atItem.uri) {
|
||||
link = `${atItem.uri}/#reply${atItem.source_id}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${atUser.nickname} @了你:${sourceContent}`,
|
||||
description,
|
||||
link,
|
||||
pubDate: parseDate(item.at_time * 1000),
|
||||
author: atUser.nickname,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站消息 - @我的`,
|
||||
link: 'https://message.bilibili.com/#/at',
|
||||
description: `${name} 的 B站消息 - @我的`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/like/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/like/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '收到的赞',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface LikeUser {
|
||||
mid: number;
|
||||
fans: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
mid_link: string;
|
||||
follow: boolean;
|
||||
}
|
||||
|
||||
interface LikeItem {
|
||||
id: number;
|
||||
users: LikeUser[];
|
||||
item: {
|
||||
item_id: number;
|
||||
pid: number;
|
||||
type: string;
|
||||
business: string;
|
||||
business_id: number;
|
||||
reply_business_id: number;
|
||||
like_business_id: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
image: string;
|
||||
uri: string;
|
||||
detail_name: string;
|
||||
native_uri: string;
|
||||
ctime: number;
|
||||
};
|
||||
counts: number;
|
||||
like_time: number;
|
||||
notice_state: number;
|
||||
}
|
||||
|
||||
interface LikeResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
latest: {
|
||||
items: LikeItem[];
|
||||
last_view_at: number;
|
||||
};
|
||||
total: {
|
||||
cursor: {
|
||||
is_end: boolean;
|
||||
id: number;
|
||||
time: number;
|
||||
};
|
||||
items: LikeItem[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await cache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
const response = await ofetch<LikeResponse>('https://api.bilibili.com/x/msgfeed/like', {
|
||||
query: {
|
||||
platform: 'web',
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message ?? `Error code ${response.code}`);
|
||||
}
|
||||
|
||||
const allItems = [...(response.data.latest?.items || []), ...(response.data.total?.items || [])];
|
||||
|
||||
// Deduplicate by id
|
||||
const uniqueItems = allItems.filter((item, index, self) => index === self.findIndex((t) => t.id === item.id));
|
||||
|
||||
const items: DataItem[] = uniqueItems.map((item) => {
|
||||
const likeUsers = item.users;
|
||||
const likeItem = item.item;
|
||||
const counts = item.counts;
|
||||
|
||||
const userNames = likeUsers.map((u) => u.nickname).join('、');
|
||||
const displayNames = counts > likeUsers.length ? `${userNames} 等 ${counts} 人` : userNames;
|
||||
|
||||
let description = `<p><strong>${displayNames}</strong> 赞了你的${likeItem.business}:</p>`;
|
||||
description += `<p><strong>${likeItem.title}</strong></p>`;
|
||||
|
||||
if (likeItem.desc) {
|
||||
description += `<blockquote>${likeItem.desc}</blockquote>`;
|
||||
}
|
||||
|
||||
if (likeItem.image) {
|
||||
description += `<p><img src="${likeItem.image.replace('http://', 'https://')}" /></p>`;
|
||||
}
|
||||
|
||||
// Generate link based on type
|
||||
let link = likeItem.uri;
|
||||
if (likeItem.type === 'reply' && likeItem.item_id) {
|
||||
link = `${likeItem.uri}/#reply${likeItem.item_id}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${displayNames} 赞了你的${likeItem.business}「${likeItem.title}」`,
|
||||
description,
|
||||
link,
|
||||
pubDate: parseDate(item.like_time * 1000),
|
||||
author: userNames,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站消息 - 收到的赞`,
|
||||
link: 'https://message.bilibili.com/#/love',
|
||||
description: `${name} 的 B站消息 - 收到的赞`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/reply/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/reply/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '回复我的',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface ReplyItem {
|
||||
id: number;
|
||||
user: {
|
||||
mid: number;
|
||||
fans: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
mid_link: string;
|
||||
follow: boolean;
|
||||
};
|
||||
item: {
|
||||
subject_id: number;
|
||||
root_id: number;
|
||||
source_id: number;
|
||||
target_id: number;
|
||||
type: string;
|
||||
business_id: number;
|
||||
business: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
image: string;
|
||||
uri: string;
|
||||
native_uri: string;
|
||||
detail_title: string;
|
||||
root_reply_content: string;
|
||||
source_content: string;
|
||||
target_reply_content: string;
|
||||
at_details: unknown[];
|
||||
hide_reply_button: boolean;
|
||||
hide_like_button: boolean;
|
||||
like_state: number;
|
||||
danmu: unknown;
|
||||
message: string;
|
||||
};
|
||||
counts: number;
|
||||
is_multi: number;
|
||||
reply_time: number;
|
||||
}
|
||||
|
||||
interface ReplyResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
cursor: {
|
||||
is_end: boolean;
|
||||
id: number;
|
||||
time: number;
|
||||
};
|
||||
items: ReplyItem[];
|
||||
last_view_at: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await cache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
const response = await ofetch<ReplyResponse>('https://api.bilibili.com/x/msgfeed/reply', {
|
||||
query: {
|
||||
platform: 'web',
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message ?? `Error code ${response.code}`);
|
||||
}
|
||||
|
||||
const items: DataItem[] = (response.data.items || []).map((item) => {
|
||||
const replyUser = item.user;
|
||||
const replyItem = item.item;
|
||||
const sourceContent = replyItem.source_content;
|
||||
const targetContent = replyItem.target_reply_content;
|
||||
const rootContent = replyItem.root_reply_content;
|
||||
|
||||
let description = `<p><strong>${replyUser.nickname}</strong> 回复了你:</p>`;
|
||||
description += `<blockquote>${sourceContent}</blockquote>`;
|
||||
|
||||
if (targetContent) {
|
||||
description += `<p>你的评论:</p><blockquote>${targetContent}</blockquote>`;
|
||||
} else if (rootContent) {
|
||||
description += `<p>你的评论:</p><blockquote>${rootContent}</blockquote>`;
|
||||
}
|
||||
|
||||
if (replyItem.image) {
|
||||
description += `<p><img src="${replyItem.image.replace('http://', 'https://')}" /></p>`;
|
||||
}
|
||||
|
||||
description += `<p>来自:${replyItem.business} - ${replyItem.title}</p>`;
|
||||
|
||||
// Generate comment link with root_id for direct navigation to the comment
|
||||
let link = replyItem.uri;
|
||||
if (replyItem.root_id && replyItem.uri) {
|
||||
link = `${replyItem.uri}/#reply${replyItem.root_id}`;
|
||||
} else if (replyItem.source_id && replyItem.uri) {
|
||||
link = `${replyItem.uri}/#reply${replyItem.source_id}`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${replyUser.nickname} 回复了你:${sourceContent}`,
|
||||
description,
|
||||
link,
|
||||
pubDate: parseDate(item.reply_time * 1000),
|
||||
author: replyUser.nickname,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站消息 - 回复我的`,
|
||||
link: 'https://message.bilibili.com/#/reply',
|
||||
description: `${name} 的 B站消息 - 回复我的`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import cache from '@/utils/cache';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import bilibiliCache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/sessions/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/sessions/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '我的消息',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface SessionItem {
|
||||
talker_id: number;
|
||||
session_type: number;
|
||||
at_seqno: number;
|
||||
top_ts: number;
|
||||
group_name: string;
|
||||
group_cover: string;
|
||||
is_follow: number;
|
||||
is_dnd: number;
|
||||
ack_seqno: number;
|
||||
ack_ts: number;
|
||||
session_ts: number;
|
||||
unread_count: number;
|
||||
last_msg: {
|
||||
sender_uid: number;
|
||||
receiver_type: number;
|
||||
receiver_id: number;
|
||||
msg_type: number;
|
||||
content: string;
|
||||
msg_seqno: number;
|
||||
timestamp: number;
|
||||
at_uids: number[] | null;
|
||||
msg_key: number;
|
||||
msg_status: number;
|
||||
notify_code: string;
|
||||
msg_source: number;
|
||||
} | null;
|
||||
group_type: number;
|
||||
can_fold: number;
|
||||
status: number;
|
||||
max_seqno: number;
|
||||
new_push_msg: number;
|
||||
setting: number;
|
||||
is_guardian: number;
|
||||
is_intercept: number;
|
||||
is_trust: number;
|
||||
system_msg_type: number;
|
||||
live_status: number;
|
||||
biz_msg_unread_count: number;
|
||||
user_label: unknown;
|
||||
}
|
||||
|
||||
interface SessionResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
session_list: SessionItem[] | null;
|
||||
has_more: number;
|
||||
anti_disturb_cleaning: boolean;
|
||||
is_address_list_empty: number;
|
||||
system_msg: Record<string, number>;
|
||||
show_level: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
mid: string;
|
||||
face: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UserCardsResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: Record<string, UserInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse message content based on msg_type
|
||||
* msg_type 1: text, 2: image, 5: recall, etc.
|
||||
*/
|
||||
function parseMessageContent(content: string, msgType: number): string {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
switch (msgType) {
|
||||
case 1: // Text message
|
||||
return parsed.content || content;
|
||||
case 2: // Image
|
||||
return `[图片] ${parsed.url || ''}`;
|
||||
case 5: // Recall
|
||||
return '[消息已撤回]';
|
||||
case 6: // Sticker
|
||||
return '[表情]';
|
||||
case 7: // Share
|
||||
return `[分享] ${parsed.title || ''}`;
|
||||
case 10: // System notification
|
||||
return parsed.content || parsed.title || content;
|
||||
case 11: // Video card
|
||||
return `[视频] ${parsed.title || ''}`;
|
||||
case 12: // Article card
|
||||
return `[专栏] ${parsed.title || ''}`;
|
||||
case 14: // Bangumi card
|
||||
return `[番剧] ${parsed.title || ''}`;
|
||||
default:
|
||||
return parsed.content || parsed.title || content;
|
||||
}
|
||||
} catch {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await bilibiliCache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
const response = await ofetch<SessionResponse>('https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions', {
|
||||
query: {
|
||||
session_type: 1,
|
||||
group_fold: 1,
|
||||
unfollow_fold: 0,
|
||||
sort_rule: 2,
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message ?? response.msg ?? `Error code ${response.code}`);
|
||||
}
|
||||
|
||||
const sessionList = response.data.session_list || [];
|
||||
const talkerIds = sessionList.filter((s) => s.session_type === 1).map((s) => s.talker_id);
|
||||
|
||||
// Fetch user info for all talkers
|
||||
let userCards: Record<string, UserInfo> = {};
|
||||
if (talkerIds.length > 0) {
|
||||
const userCardsResponse = await cache.tryGet(
|
||||
`bilibili-user-cards-${talkerIds.join(',')}`,
|
||||
async () => {
|
||||
const res = await ofetch<UserCardsResponse>('https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards', {
|
||||
query: {
|
||||
uids: talkerIds.join(','),
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
return res.data || {};
|
||||
},
|
||||
config.cache.routeExpire
|
||||
);
|
||||
userCards = userCardsResponse as Record<string, UserInfo>;
|
||||
}
|
||||
|
||||
const items: DataItem[] = sessionList
|
||||
.filter((session) => session.last_msg)
|
||||
.map((session) => {
|
||||
const lastMsg = session.last_msg!;
|
||||
const talkerId = session.talker_id;
|
||||
const userInfo = userCards[String(talkerId)];
|
||||
const talkerName = userInfo?.name || `用户${talkerId}`;
|
||||
const talkerFace = userInfo?.face || '';
|
||||
|
||||
const msgContent = parseMessageContent(lastMsg.content, lastMsg.msg_type);
|
||||
const isSentByMe = lastMsg.sender_uid === Number(uid);
|
||||
|
||||
let description = '';
|
||||
if (talkerFace) {
|
||||
description += `<p><img src="${talkerFace.replace('http://', 'https://')}" width="48" height="48" style="border-radius: 50%;" /></p>`;
|
||||
}
|
||||
|
||||
description += isSentByMe ? `<p><strong>你</strong> 对 <strong>${talkerName}</strong> 说:</p>` : `<p><strong>${talkerName}</strong> 说:</p>`;
|
||||
description += `<blockquote>${msgContent}</blockquote>`;
|
||||
|
||||
if (session.unread_count > 0) {
|
||||
description += `<p>未读消息: ${session.unread_count} 条</p>`;
|
||||
}
|
||||
|
||||
const title = isSentByMe ? `你对 ${talkerName} 说:${msgContent}` : `${talkerName}:${msgContent}`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
link: `https://message.bilibili.com/#/whisper/mid${talkerId}`,
|
||||
pubDate: parseDate(lastMsg.timestamp * 1000),
|
||||
author: isSentByMe ? name : talkerName,
|
||||
guid: `bilibili-session-${talkerId}-${lastMsg.msg_key}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站消息 - 我的消息`,
|
||||
link: 'https://message.bilibili.com/#/whisper',
|
||||
description: `${name} 的 B站消息 - 我的消息`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/system/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/system/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '系统通知',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface SystemNotifyItem {
|
||||
id: number;
|
||||
cursor: number;
|
||||
publisher: {
|
||||
name: string;
|
||||
mid: number;
|
||||
face: string;
|
||||
};
|
||||
type: number;
|
||||
title: string;
|
||||
content: string;
|
||||
source: {
|
||||
name: string;
|
||||
logo: string;
|
||||
};
|
||||
time_at: string;
|
||||
card_type: number;
|
||||
card_brief: string;
|
||||
card_msg_brief: string;
|
||||
card_cover: string;
|
||||
card_story_title: string;
|
||||
card_link: string;
|
||||
mc: string;
|
||||
is_station: number;
|
||||
is_send: number;
|
||||
notify_cursor: number;
|
||||
}
|
||||
|
||||
interface SystemResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
system_notify_list: SystemNotifyItem[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse bilibili message content with special link format
|
||||
* Format: #{text}{"url"} -> <a href="url">text</a>
|
||||
*/
|
||||
function parseMessageContent(content: string): string {
|
||||
// Match pattern like #{text}{"url"}
|
||||
const linkPattern = /#\{([^}]+)\}\{"([^"]+)"\}/g;
|
||||
return content.replaceAll(linkPattern, '<a href="$2">$1</a>');
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await cache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
const response = await ofetch<SystemResponse>('https://message.bilibili.com/x/sys-msg/query_user_notify', {
|
||||
query: {
|
||||
page_size: 20,
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(response.message ?? response.msg ?? `Error code ${response.code}`);
|
||||
}
|
||||
|
||||
const items: DataItem[] = (response.data.system_notify_list || []).map((item) => {
|
||||
let description = `<p><strong>${item.title}</strong></p>`;
|
||||
const parsedContent = parseMessageContent(item.content);
|
||||
description += `<p>${parsedContent.replaceAll('\n', '<br>')}</p>`;
|
||||
|
||||
if (item.source.logo) {
|
||||
description += `<p><img src="${item.source.logo.replace('http://', 'https://')}" width="40" /></p>`;
|
||||
}
|
||||
|
||||
if (item.card_cover) {
|
||||
description += `<p><img src="${item.card_cover.replace('http://', 'https://')}" /></p>`;
|
||||
}
|
||||
|
||||
const link = item.card_link || 'https://message.bilibili.com/#/system';
|
||||
|
||||
return {
|
||||
title: item.title,
|
||||
description,
|
||||
link,
|
||||
pubDate: parseDate(item.time_at),
|
||||
guid: `bilibili-system-notify-${item.id}-${item.cursor}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站消息 - 系统通知`,
|
||||
link: 'https://message.bilibili.com/#/system',
|
||||
description: `${name} 的 B站消息 - 系统通知`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import type { DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/message/unread/:uid',
|
||||
categories: ['social-media'],
|
||||
example: '/bilibili/message/unread/2267573',
|
||||
parameters: { uid: '用户 id' },
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'BILIBILI_COOKIE_*',
|
||||
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式:
|
||||
1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
|
||||
2. 打开控制台,切换到 Network 面板,刷新
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
name: '未读消息',
|
||||
maintainers: ['pilgrimlyieu'],
|
||||
handler,
|
||||
description: `:::warning
|
||||
用户消息需要 b 站登录后的 Cookie 值,所以只能自建,详情见部署页面的配置模块。
|
||||
|
||||
此路由返回所有未读消息类型的汇总状态。
|
||||
:::`,
|
||||
};
|
||||
|
||||
interface UnreadMsgResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
at: number;
|
||||
coin: number;
|
||||
danmu: number;
|
||||
favorite: number;
|
||||
like: number;
|
||||
recv_like: number;
|
||||
recv_reply: number;
|
||||
reply: number;
|
||||
sys_msg: number;
|
||||
sys_msg_style: number;
|
||||
up: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UnreadSessionResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
message: string;
|
||||
ttl: number;
|
||||
data: {
|
||||
unfollow_unread: number;
|
||||
follow_unread: number;
|
||||
unfollow_push_msg: number;
|
||||
dustbin_push_msg: number;
|
||||
dustbin_unread: number;
|
||||
biz_msg_unfollow_unread: number;
|
||||
biz_msg_follow_unread: number;
|
||||
custom_unread: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(ctx) {
|
||||
const uid = ctx.req.param('uid');
|
||||
const name = await cache.getUsernameFromUID(uid);
|
||||
|
||||
const cookie = config.bilibili.cookies[uid];
|
||||
if (cookie === undefined) {
|
||||
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
|
||||
}
|
||||
|
||||
// Fetch message unread counts
|
||||
const [msgUnread, sessionUnread] = await Promise.all([
|
||||
ofetch<UnreadMsgResponse>('https://api.vc.bilibili.com/x/im/web/msgfeed/unread', {
|
||||
query: {
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
}),
|
||||
ofetch<UnreadSessionResponse>('https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread', {
|
||||
query: {
|
||||
unread_type: 0,
|
||||
show_dustbin: 1,
|
||||
build: 0,
|
||||
mobi_app: 'web',
|
||||
},
|
||||
headers: {
|
||||
Referer: 'https://message.bilibili.com/',
|
||||
Cookie: cookie,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (msgUnread.code !== 0) {
|
||||
throw new Error(msgUnread.message ?? `Error code ${msgUnread.code}`);
|
||||
}
|
||||
|
||||
const msgData = msgUnread.data;
|
||||
const sessionData = sessionUnread.data;
|
||||
|
||||
const items: DataItem[] = [];
|
||||
const now = new Date();
|
||||
|
||||
// 回复我的
|
||||
if (msgData.recv_reply > 0 || msgData.reply > 0) {
|
||||
const replyCount = msgData.recv_reply || msgData.reply;
|
||||
items.push({
|
||||
title: `回复我的:${replyCount} 条未读`,
|
||||
description: `<p>你有 <strong>${replyCount}</strong> 条未读回复消息</p><p><a href="https://message.bilibili.com/#/reply">点击查看</a></p>`,
|
||||
link: 'https://message.bilibili.com/#/reply',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-reply-${uid}-${replyCount}`,
|
||||
});
|
||||
}
|
||||
|
||||
// @我的
|
||||
if (msgData.at > 0) {
|
||||
items.push({
|
||||
title: `@我的:${msgData.at} 条未读`,
|
||||
description: `<p>你有 <strong>${msgData.at}</strong> 条未读@消息</p><p><a href="https://message.bilibili.com/#/at">点击查看</a></p>`,
|
||||
link: 'https://message.bilibili.com/#/at',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-at-${uid}-${msgData.at}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 收到的赞
|
||||
if (msgData.recv_like > 0 || msgData.like > 0) {
|
||||
const likeCount = msgData.recv_like || msgData.like;
|
||||
items.push({
|
||||
title: `收到的赞:${likeCount} 条未读`,
|
||||
description: `<p>你有 <strong>${likeCount}</strong> 条未读点赞消息</p><p><a href="https://message.bilibili.com/#/love">点击查看</a></p>`,
|
||||
link: 'https://message.bilibili.com/#/love',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-like-${uid}-${likeCount}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 系统通知
|
||||
if (msgData.sys_msg > 0) {
|
||||
items.push({
|
||||
title: `系统通知:${msgData.sys_msg} 条未读`,
|
||||
description: `<p>你有 <strong>${msgData.sys_msg}</strong> 条未读系统通知</p><p><a href="https://message.bilibili.com/#/system">点击查看</a></p>`,
|
||||
link: 'https://message.bilibili.com/#/system',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-system-${uid}-${msgData.sys_msg}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 私信
|
||||
const privateUnread = (sessionData?.follow_unread || 0) + (sessionData?.unfollow_unread || 0);
|
||||
if (privateUnread > 0) {
|
||||
items.push({
|
||||
title: `私信:${privateUnread} 条未读`,
|
||||
description: `<p>你有 <strong>${privateUnread}</strong> 条未读私信(已关注: ${sessionData?.follow_unread || 0},未关注: ${sessionData?.unfollow_unread || 0})</p><p><a href="https://message.bilibili.com/#/whisper">点击查看</a></p>`,
|
||||
link: 'https://message.bilibili.com/#/whisper',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-session-${uid}-${privateUnread}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 投币
|
||||
if (msgData.coin > 0) {
|
||||
items.push({
|
||||
title: `收到的投币:${msgData.coin} 条未读`,
|
||||
description: `<p>你有 <strong>${msgData.coin}</strong> 条未读投币消息</p>`,
|
||||
link: 'https://message.bilibili.com/',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-coin-${uid}-${msgData.coin}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 收藏
|
||||
if (msgData.favorite > 0) {
|
||||
items.push({
|
||||
title: `收到的收藏:${msgData.favorite} 条未读`,
|
||||
description: `<p>你有 <strong>${msgData.favorite}</strong> 条未读收藏消息</p>`,
|
||||
link: 'https://message.bilibili.com/',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-favorite-${uid}-${msgData.favorite}`,
|
||||
});
|
||||
}
|
||||
|
||||
// UP主助手 messages
|
||||
if (msgData.up > 0) {
|
||||
items.push({
|
||||
title: `UP主助手:${msgData.up} 条未读`,
|
||||
description: `<p>你有 <strong>${msgData.up}</strong> 条未读UP主助手消息</p>`,
|
||||
link: 'https://message.bilibili.com/',
|
||||
pubDate: now,
|
||||
guid: `bilibili-unread-up-${uid}-${msgData.up}`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${name} 的 B站未读消息`,
|
||||
link: 'https://message.bilibili.com/',
|
||||
description: `${name} 的 B站未读消息汇总`,
|
||||
item: items,
|
||||
allowEmpty: true,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue