dsh-shlr-bilitoolkit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +122 -3
  2. package/cordis.patch.yml +13 -0
  3. package/dist/apis/article.d.ts +12 -0
  4. package/dist/apis/article.js +49 -0
  5. package/dist/apis/auth.d.ts +20 -0
  6. package/dist/apis/auth.js +83 -0
  7. package/dist/apis/comment.d.ts +37 -0
  8. package/dist/apis/comment.js +164 -0
  9. package/dist/apis/download.d.ts +8 -0
  10. package/dist/apis/download.js +60 -0
  11. package/dist/apis/dynamic.d.ts +23 -0
  12. package/dist/apis/dynamic.js +154 -0
  13. package/dist/apis/interact.d.ts +29 -0
  14. package/dist/apis/interact.js +224 -0
  15. package/dist/apis/message.d.ts +18 -0
  16. package/dist/apis/message.js +109 -0
  17. package/dist/apis/notify.d.ts +17 -0
  18. package/dist/apis/notify.js +54 -0
  19. package/dist/apis/profile.d.ts +15 -0
  20. package/dist/apis/profile.js +59 -0
  21. package/dist/apis/relation.d.ts +13 -0
  22. package/dist/apis/relation.js +47 -0
  23. package/dist/apis/search.d.ts +17 -0
  24. package/dist/apis/search.js +81 -0
  25. package/dist/apis/space.d.ts +17 -0
  26. package/dist/apis/space.js +89 -0
  27. package/dist/apis/video.d.ts +18 -0
  28. package/dist/apis/video.js +153 -0
  29. package/dist/cache.d.ts +18 -0
  30. package/dist/cache.js +75 -0
  31. package/dist/client.d.ts +85 -0
  32. package/dist/client.js +325 -0
  33. package/dist/dsh.d.ts +25 -0
  34. package/dist/dsh.js +484 -0
  35. package/dist/endpoints.d.ts +55 -0
  36. package/dist/endpoints.js +69 -0
  37. package/dist/errors.d.ts +18 -0
  38. package/dist/errors.js +50 -0
  39. package/dist/formatting.d.ts +7 -0
  40. package/dist/formatting.js +69 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.js +21 -0
  43. package/dist/models.d.ts +215 -0
  44. package/dist/models.js +876 -0
  45. package/dist/pagination.d.ts +7 -0
  46. package/dist/pagination.js +20 -0
  47. package/dist/util.d.ts +2 -0
  48. package/dist/util.js +4 -0
  49. package/dist/wbi.d.ts +21 -0
  50. package/dist/wbi.js +105 -0
  51. package/package.json +52 -6
@@ -0,0 +1,154 @@
1
+ /** 动态:关注动态流、用户空间动态,以及发布 / 转发 / 点赞 / 删除。
2
+
3
+ 读取走网页版 polymer 接口,需要登录态。写操作(publish / repost / thumb /
4
+ delete)都是公开行为,受风控和人机验证限制,使用前请自行确认。
5
+ */
6
+ import * as endpoints from "../endpoints";
7
+ import { BiliApiError } from "../errors";
8
+ import { Dynamic } from "../models";
9
+ const FEATURES = "itemOpusStyle";
10
+ const REFERER = "https://t.bilibili.com/";
11
+ export class DynamicAPI {
12
+ client;
13
+ constructor(client) {
14
+ this.client = client;
15
+ this._detailCache = {};
16
+ }
17
+ _detailCache;
18
+ async detail(dynamicId) {
19
+ const id = String(Math.floor(Number(dynamicId)));
20
+ const cached = this._detailCache[id];
21
+ if (cached)
22
+ return cached;
23
+ const payload = await this.client.get(endpoints.DYNAMIC_DETAIL, { id, features: FEATURES }, { Referer: `https://t.bilibili.com/${id}` });
24
+ const dynamic = Dynamic.fromApi((payload?.data ?? {}).item);
25
+ if (!dynamic)
26
+ throw new BiliApiError("未找到该动态");
27
+ this._detailCache[id] = dynamic;
28
+ return dynamic;
29
+ }
30
+ async feed(page = 1, offset, type = "all") {
31
+ const params = {
32
+ type,
33
+ page,
34
+ timezone_offset: -480,
35
+ features: FEATURES,
36
+ };
37
+ if (offset)
38
+ params["offset"] = offset;
39
+ const payload = await this.client.get(endpoints.DYNAMIC_FEED_ALL, params, { Referer: "https://t.bilibili.com/" });
40
+ const data = payload?.data ?? {};
41
+ return [
42
+ DynamicAPI.parseItems(data),
43
+ String(data.offset ?? ""),
44
+ Boolean(data.has_more),
45
+ ];
46
+ }
47
+ async space(uid, offset = "") {
48
+ const payload = await this.client.get(endpoints.DYNAMIC_FEED_SPACE, { host_mid: Number(uid), offset: offset || "", features: FEATURES }, { Referer: `https://space.bilibili.com/${uid}/dynamic` });
49
+ const data = payload?.data ?? {};
50
+ return [
51
+ DynamicAPI.parseItems(data),
52
+ String(data.offset ?? ""),
53
+ Boolean(data.has_more),
54
+ Number(data.total ?? 0),
55
+ ];
56
+ }
57
+ async *iterFeed(maxPages, type = "all") {
58
+ let page = 1;
59
+ let offset;
60
+ while (maxPages == null || page <= maxPages) {
61
+ const [items, next, hasMore] = await this.feed(page, offset, type);
62
+ if (!items.length)
63
+ return;
64
+ for (const item of items)
65
+ yield item;
66
+ if (!hasMore)
67
+ return;
68
+ offset = next;
69
+ page += 1;
70
+ }
71
+ }
72
+ async *iterSpace(uid, maxPages) {
73
+ let page = 1;
74
+ let offset = "";
75
+ while (maxPages == null || page <= maxPages) {
76
+ const [items, next, hasMore] = await this.space(uid, offset);
77
+ if (!items.length)
78
+ return;
79
+ for (const item of items)
80
+ yield item;
81
+ if (!hasMore)
82
+ return;
83
+ offset = next;
84
+ page += 1;
85
+ }
86
+ }
87
+ // ------------------------------------------------------------------ 写操作
88
+ async publish(text, atUids, ctrl, closeComment = false) {
89
+ text = (text || "").trim();
90
+ if (!text)
91
+ throw new Error("动态内容不能为空");
92
+ const extension = { emoji_type: 1, from: { emoji_type: 1 }, flag_cfg: {} };
93
+ const form = {
94
+ dynamic_id: 0,
95
+ type: 4,
96
+ rid: 0,
97
+ content: text,
98
+ up_choose_comment: 0,
99
+ up_close_comment: closeComment ? 1 : 0,
100
+ extension: JSON.stringify(extension),
101
+ at_uids: (atUids ?? []).map(String).join(","),
102
+ ctrl: JSON.stringify(ctrl ?? []),
103
+ csrf: this.client.csrf,
104
+ csrf_token: this.client.csrf,
105
+ };
106
+ const payload = await this.client.post(endpoints.DYNAMIC_CREATE, form, { Referer: REFERER });
107
+ return DynamicAPI.dynamicId(payload);
108
+ }
109
+ async repost(dynamicId, text = "") {
110
+ const form = {
111
+ dynamic_id: String(Math.floor(Number(dynamicId))),
112
+ content: text || "",
113
+ csrf: this.client.csrf,
114
+ csrf_token: this.client.csrf,
115
+ };
116
+ const payload = await this.client.post(endpoints.DYNAMIC_REPUBLISH, form, { Referer: REFERER });
117
+ return DynamicAPI.dynamicId(payload);
118
+ }
119
+ async thumb(dynamicId, liked = true) {
120
+ const body = JSON.stringify({
121
+ dyn_id_str: String(Math.floor(Number(dynamicId))),
122
+ up: liked ? 1 : 2,
123
+ });
124
+ await this.client.post(endpoints.DYNAMIC_THUMB, body, {
125
+ "Content-Type": "application/json",
126
+ Referer: REFERER,
127
+ }, false, { csrf: this.client.csrf }, true);
128
+ }
129
+ async delete(dynamicId) {
130
+ const form = {
131
+ dynamic_id: String(Math.floor(Number(dynamicId))),
132
+ csrf: this.client.csrf,
133
+ csrf_token: this.client.csrf,
134
+ };
135
+ await this.client.post(endpoints.DYNAMIC_DELETE, form, { Referer: REFERER });
136
+ }
137
+ static dynamicId(payload) {
138
+ const data = payload?.data ?? {};
139
+ const value = data.dynamic_id_str ?? data.dynamic_id;
140
+ if (!value) {
141
+ throw new BiliApiError("动态接口未返回 dynamic_id,可能被风控拦截");
142
+ }
143
+ return String(value);
144
+ }
145
+ static parseItems(data) {
146
+ const items = [];
147
+ for (const item of data?.items ?? []) {
148
+ const dynamic = Dynamic.fromApi(item);
149
+ if (dynamic)
150
+ items.push(dynamic);
151
+ }
152
+ return items;
153
+ }
154
+ }
@@ -0,0 +1,29 @@
1
+ /** 互动接口:点赞、收藏、关注(真实写操作,需要登录并依赖 bili_jct)。 */
2
+ import type { BiliClient } from "../client";
3
+ import { Video } from "../models";
4
+ export declare class InteractAPI {
5
+ private client;
6
+ constructor(client: BiliClient);
7
+ private static videoHeaders;
8
+ like(bvid?: string, aid?: number, liked?: boolean): Promise<void>;
9
+ hasLike(bvid?: string, aid?: number): Promise<boolean>;
10
+ folders(): Promise<Array<{
11
+ id: number;
12
+ name: string;
13
+ count: number;
14
+ }>>;
15
+ folderVideos(folderId: number, page?: number, pageSize?: number, order?: string): Promise<[Video[], number]>;
16
+ iterFolderVideos(folderId: number, pageSize?: number, order?: string, maxPages?: number): AsyncGenerator<Video, any, any>;
17
+ createFolder(title: string, privacy?: number): Promise<number>;
18
+ deleteFolder(folderId: number): Promise<void>;
19
+ favorite(bvid: string, folderId?: number, folderName?: string): Promise<number>;
20
+ unfavorite(bvid: string, folderId?: number, folderName?: string): Promise<number>;
21
+ coin(bvid?: string, aid?: number, count?: number, alsoLike?: boolean): Promise<void>;
22
+ coinsGiven(bvid?: string, aid?: number): Promise<number>;
23
+ triple(bvid: string, coinCount?: number, folderName?: string): Promise<Record<string, any>>;
24
+ follow(uid: number, follow?: boolean): Promise<void>;
25
+ isFollowing(uid: number): Promise<boolean>;
26
+ private mid;
27
+ private resolveAid;
28
+ private pickFolder;
29
+ }
@@ -0,0 +1,224 @@
1
+ /** 互动接口:点赞、收藏、关注(真实写操作,需要登录并依赖 bili_jct)。 */
2
+ import * as endpoints from "../endpoints";
3
+ import { BiliApiError, CookieError } from "../errors";
4
+ import { Video } from "../models";
5
+ import { iterPages } from "../pagination";
6
+ export class InteractAPI {
7
+ client;
8
+ constructor(client) {
9
+ this.client = client;
10
+ }
11
+ static videoHeaders(bvid) {
12
+ if (!bvid)
13
+ return undefined;
14
+ return { Referer: `https://www.bilibili.com/video/${bvid}/` };
15
+ }
16
+ // ------------------------------------------------------------------ 点赞
17
+ async like(bvid, aid, liked = true) {
18
+ const target = await this.resolveAid(bvid, aid);
19
+ await this.client.post(endpoints.VIDEO_LIKE, { aid: target, like: liked ? 1 : 2, csrf: this.client.csrf }, InteractAPI.videoHeaders(bvid));
20
+ }
21
+ async hasLike(bvid, aid) {
22
+ const target = await this.resolveAid(bvid, aid);
23
+ const payload = await this.client.get(endpoints.VIDEO_HAS_LIKE, { aid: target }, InteractAPI.videoHeaders(bvid));
24
+ const data = payload?.data;
25
+ if (data && typeof data === "object") {
26
+ return Boolean(data.like ?? data.has_like);
27
+ }
28
+ return Boolean(data);
29
+ }
30
+ // ------------------------------------------------------------------ 收藏
31
+ async folders() {
32
+ const payload = await this.client.get(endpoints.FAV_FOLDERS, {
33
+ up_mid: await this.mid(),
34
+ type: 2,
35
+ });
36
+ const items = ((payload?.data ?? {}).list ?? []);
37
+ return items
38
+ .filter((i) => i && i.id)
39
+ .map((i) => ({
40
+ id: Number(i.id),
41
+ name: i.title ?? i.name ?? "",
42
+ count: i.media_count ?? 0,
43
+ }));
44
+ }
45
+ async folderVideos(folderId, page = 1, pageSize = 20, order = "mtime") {
46
+ if (!["mtime", "view", "pubtime"].includes(order)) {
47
+ throw new Error("order 只支持 mtime / view / pubtime");
48
+ }
49
+ const payload = await this.client.get(endpoints.FAV_RESOURCE_LIST, {
50
+ media_id: Number(folderId),
51
+ pn: page,
52
+ ps: pageSize,
53
+ order,
54
+ type: 0,
55
+ tid: 0,
56
+ platform: "web",
57
+ }, {
58
+ Referer: `https://space.bilibili.com/${await this.mid()}/favlist?fid=${folderId}`,
59
+ });
60
+ const data = payload?.data ?? {};
61
+ const videos = [];
62
+ for (const item of data.medias ?? []) {
63
+ const v = Video.fromFavoriteItem(item);
64
+ if (v)
65
+ videos.push(v);
66
+ }
67
+ const total = (data.info ?? {}).media_count ?? videos.length;
68
+ return [videos, total];
69
+ }
70
+ iterFolderVideos(folderId, pageSize = 20, order = "mtime", maxPages) {
71
+ const fetch = (page) => this.folderVideos(folderId, page, pageSize, order);
72
+ return iterPages(fetch, maxPages);
73
+ }
74
+ async createFolder(title, privacy = 0) {
75
+ title = (title || "").trim();
76
+ if (!title)
77
+ throw new Error("收藏夹名称不能为空");
78
+ const payload = await this.client.post(endpoints.FAV_FOLDER_ADD, {
79
+ title,
80
+ privacy,
81
+ csrf: this.client.csrf,
82
+ });
83
+ const folderId = (payload?.data ?? {}).id;
84
+ if (!folderId)
85
+ throw new BiliApiError("创建收藏夹失败:接口未返回 id");
86
+ return Number(folderId);
87
+ }
88
+ async deleteFolder(folderId) {
89
+ await this.client.post(endpoints.FAV_FOLDER_DEL, {
90
+ media_ids: Number(folderId),
91
+ csrf: this.client.csrf,
92
+ });
93
+ }
94
+ async favorite(bvid, folderId, folderName) {
95
+ const aid = await this.resolveAid(bvid, null);
96
+ const fid = folderId ?? (await this.pickFolder(folderName));
97
+ await this.client.post(endpoints.FAV_DEAL, {
98
+ rid: aid,
99
+ type: 2, // 2 = 视频
100
+ add_media_ids: fid,
101
+ del_media_ids: "",
102
+ csrf: this.client.csrf,
103
+ }, InteractAPI.videoHeaders(bvid));
104
+ return Number(fid);
105
+ }
106
+ async unfavorite(bvid, folderId, folderName) {
107
+ const aid = await this.resolveAid(bvid, null);
108
+ const fid = folderId ?? (await this.pickFolder(folderName));
109
+ await this.client.post(endpoints.FAV_DEAL, {
110
+ rid: aid,
111
+ type: 2,
112
+ add_media_ids: "",
113
+ del_media_ids: fid,
114
+ csrf: this.client.csrf,
115
+ }, InteractAPI.videoHeaders(bvid));
116
+ return Number(fid);
117
+ }
118
+ // ------------------------------------------------------------------ 投币
119
+ async coin(bvid, aid, count = 1, alsoLike = false) {
120
+ if (![1, 2].includes(count))
121
+ throw new Error("投币数只能是 1 或 2");
122
+ const target = await this.resolveAid(bvid, aid);
123
+ await this.client.post(endpoints.COIN_ADD, {
124
+ aid: target,
125
+ multiply: count,
126
+ select_like: alsoLike ? 1 : 0,
127
+ csrf: this.client.csrf,
128
+ }, InteractAPI.videoHeaders(bvid));
129
+ }
130
+ async coinsGiven(bvid, aid) {
131
+ const target = await this.resolveAid(bvid, aid);
132
+ const payload = await this.client.get(endpoints.COIN_QUERY, { aid: target }, InteractAPI.videoHeaders(bvid));
133
+ return Number((payload?.data ?? {}).multiply ?? 0);
134
+ }
135
+ async triple(bvid, coinCount = 2, folderName) {
136
+ const result = {};
137
+ try {
138
+ if (await this.hasLike(bvid)) {
139
+ result["like"] = { ok: true, status: "skipped", message: "已点赞" };
140
+ }
141
+ else {
142
+ await this.like(bvid, undefined, true);
143
+ result["like"] = { ok: true, status: "done", message: "已点赞" };
144
+ }
145
+ }
146
+ catch (exc) {
147
+ result["like"] = excInfo(exc);
148
+ }
149
+ try {
150
+ const given = await this.coinsGiven(bvid);
151
+ const missing = coinCount - given;
152
+ if (missing <= 0) {
153
+ result["coin"] = { ok: true, status: "skipped", message: `已投 ${given} 币` };
154
+ }
155
+ else {
156
+ await this.coin(bvid, undefined, missing);
157
+ result["coin"] = { ok: true, status: "done", message: `投币 ${missing}` };
158
+ }
159
+ }
160
+ catch (exc) {
161
+ result["coin"] = excInfo(exc);
162
+ }
163
+ try {
164
+ const fid = await this.pickFolder(folderName);
165
+ await this.favorite(bvid, fid);
166
+ result["favorite"] = { ok: true, status: "done", message: `收藏到 ${fid}` };
167
+ }
168
+ catch (exc) {
169
+ result["favorite"] = excInfo(exc);
170
+ }
171
+ return result;
172
+ }
173
+ // ------------------------------------------------------------------ 关注
174
+ async follow(uid, follow = true) {
175
+ await this.client.post(endpoints.RELATION_MODIFY, {
176
+ fid: Number(uid),
177
+ act: follow ? 1 : 2,
178
+ re_src: 11,
179
+ csrf: this.client.csrf,
180
+ }, { Referer: `https://space.bilibili.com/${Number(uid)}/` });
181
+ }
182
+ async isFollowing(uid) {
183
+ const payload = await this.client.get(endpoints.RELATION, { fid: Number(uid) }, { Referer: `https://space.bilibili.com/${Number(uid)}/` });
184
+ // attribute: 0 未关注 / 2 已关注 / 6 互关 / 128 拉黑
185
+ return [2, 6].includes((payload?.data ?? {}).attribute);
186
+ }
187
+ // ------------------------------------------------------------------ 内部
188
+ async mid() {
189
+ const account = await this.client.getAccount();
190
+ const m = account?.mid;
191
+ if (!m) {
192
+ throw new CookieError("未登录,无法操作收藏夹,请先登录");
193
+ }
194
+ return Number(m);
195
+ }
196
+ async resolveAid(bvid, aid) {
197
+ if (aid)
198
+ return Number(aid);
199
+ if (!bvid)
200
+ throw new Error("bvid 和 aid 至少提供一个");
201
+ return this.client.video.resolveAid(bvid);
202
+ }
203
+ async pickFolder(folderName) {
204
+ const folders = await this.folders();
205
+ if (!folders.length)
206
+ throw new BiliApiError("账号下没有可用的收藏夹");
207
+ if (folderName) {
208
+ for (const f of folders)
209
+ if (f.name === folderName)
210
+ return f.id;
211
+ throw new BiliApiError(`未找到收藏夹: ${folderName}`);
212
+ }
213
+ for (const f of folders)
214
+ if (f.name === "默认收藏夹")
215
+ return f.id;
216
+ return folders[0].id;
217
+ }
218
+ }
219
+ function excInfo(exc) {
220
+ if (exc instanceof BiliApiError) {
221
+ return { ok: false, status: "error", message: exc.message, code: exc.code };
222
+ }
223
+ return { ok: false, status: "error", message: String(exc) };
224
+ }
@@ -0,0 +1,18 @@
1
+ /** 私信接口:发送、撤回、聊天记录、未读数。
2
+
3
+ web 端私信要求带一个 UUID 形式的 msg[dev_id];同一 client 实例内复用,
4
+ 避免每次都被当成新设备。dev_id 生成本次会话内稳定即可(不落盘)。
5
+ */
6
+ import type { BiliClient } from "../client";
7
+ import { PrivateMessage } from "../models";
8
+ export declare function generateDevId(): string;
9
+ export declare class MessageAPI {
10
+ private client;
11
+ constructor(client: BiliClient, devId?: string);
12
+ readonly devId: string;
13
+ private senderUid;
14
+ send(receiverUid: number, content: string, msgType?: number, newFaceVersion?: number): Promise<number>;
15
+ recall(receiverUid: number, msgKey: number): Promise<void>;
16
+ history(talkerUid: number, size?: number, sessionType?: number): Promise<PrivateMessage[]>;
17
+ unread(): Promise<Record<string, unknown>>;
18
+ }
@@ -0,0 +1,109 @@
1
+ /** 私信接口:发送、撤回、聊天记录、未读数。
2
+
3
+ web 端私信要求带一个 UUID 形式的 msg[dev_id];同一 client 实例内复用,
4
+ 避免每次都被当成新设备。dev_id 生成本次会话内稳定即可(不落盘)。
5
+ */
6
+ import { randomInt } from "node:crypto";
7
+ import * as endpoints from "../endpoints";
8
+ import { BiliApiError, CookieError } from "../errors";
9
+ import { PrivateMessage } from "../models";
10
+ const DEV_ID_TEMPLATE = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
11
+ const IM_HEADERS = {
12
+ Referer: "https://message.bilibili.com/",
13
+ Origin: "https://message.bilibili.com",
14
+ };
15
+ export function generateDevId() {
16
+ let out = "";
17
+ for (const ch of DEV_ID_TEMPLATE) {
18
+ if (ch === "-")
19
+ out += ch;
20
+ else if (ch === "x")
21
+ out += randomInt(0, 16).toString(16).toUpperCase();
22
+ else if (ch === "y") {
23
+ // UUID v4 变体位,取 8~B
24
+ out += ((randomInt(0, 16) & 3) | 8).toString(16).toUpperCase();
25
+ }
26
+ else
27
+ out += ch; // 固定的版本号 "4"
28
+ }
29
+ return out;
30
+ }
31
+ export class MessageAPI {
32
+ client;
33
+ constructor(client, devId) {
34
+ this.client = client;
35
+ this.devId = devId ?? generateDevId();
36
+ }
37
+ devId;
38
+ async senderUid() {
39
+ const account = await this.client.getAccount();
40
+ const mid = account?.mid;
41
+ if (!mid) {
42
+ throw new CookieError("未登录,无法使用私信功能,请先登录");
43
+ }
44
+ return Number(mid);
45
+ }
46
+ async send(receiverUid, content, msgType = 1, newFaceVersion = 1) {
47
+ content = (content || "").trim();
48
+ if (!content)
49
+ throw new Error("私信内容不能为空");
50
+ const sender = await this.senderUid();
51
+ const form = {
52
+ "msg[sender_uid]": sender,
53
+ "msg[receiver_id]": Number(receiverUid),
54
+ "msg[receiver_type]": 1,
55
+ "msg[msg_type]": msgType,
56
+ "msg[msg_status]": 0,
57
+ "msg[new_face_version]": newFaceVersion,
58
+ "msg[dev_id]": this.devId,
59
+ "msg[timestamp]": Math.floor(Date.now() / 1000),
60
+ "msg[content]": JSON.stringify({ content }),
61
+ csrf: this.client.csrf,
62
+ };
63
+ const payload = await this.client.post(endpoints.IM_SEND, form, IM_HEADERS);
64
+ const data = payload?.data ?? {};
65
+ const msgKey = data.msg_key;
66
+ if (!msgKey) {
67
+ throw new BiliApiError("私信接口未返回 msg_key,可能被风控拦截");
68
+ }
69
+ return Number(msgKey);
70
+ }
71
+ async recall(receiverUid, msgKey) {
72
+ const sender = await this.senderUid();
73
+ const form = {
74
+ "msg[sender_uid]": sender,
75
+ "msg[receiver_id]": Number(receiverUid),
76
+ "msg[receiver_type]": 1,
77
+ "msg[msg_type]": 5,
78
+ "msg[msg_status]": 0,
79
+ "msg[dev_id]": this.devId,
80
+ "msg[timestamp]": Math.floor(Date.now() / 1000),
81
+ "msg[content]": String(msgKey),
82
+ csrf: this.client.csrf,
83
+ };
84
+ await this.client.post(endpoints.IM_SEND, form, IM_HEADERS);
85
+ }
86
+ async history(talkerUid, size = 20, sessionType = 1) {
87
+ const payload = await this.client.get(endpoints.IM_FETCH, {
88
+ sender_device_id: 1,
89
+ talker_id: Number(talkerUid),
90
+ session_type: sessionType,
91
+ size,
92
+ build: 0,
93
+ mobi_app: "web",
94
+ }, IM_HEADERS);
95
+ const data = payload?.data;
96
+ const messages = data && typeof data === "object" ? data.messages : [];
97
+ const result = (messages ?? [])
98
+ .filter((i) => i && typeof i === "object")
99
+ .map((i) => PrivateMessage.fromApi(i));
100
+ result.sort((a, b) => {
101
+ return (a.seqno || 0) - (b.seqno || 0) || (a.timestamp || 0) - (b.timestamp || 0);
102
+ });
103
+ return result;
104
+ }
105
+ async unread() {
106
+ const payload = await this.client.get(endpoints.IM_UNREAD, undefined, IM_HEADERS);
107
+ return payload?.data ?? {};
108
+ }
109
+ }
@@ -0,0 +1,17 @@
1
+ /** 消息中心:未读数、回复我的、@我的、收到的赞。
2
+
3
+ 注意:这类列表接口的分页游标(cursor.id)实测传回去仍返回同一页,
4
+ 所以这里只保证「最近一页」;更早的消息请到网页端查看。
5
+ */
6
+ import type { BiliClient } from "../client";
7
+ import { Notice } from "../models";
8
+ export declare class NotifyAPI {
9
+ private client;
10
+ constructor(client: BiliClient);
11
+ unread(): Promise<Record<string, unknown>>;
12
+ replies(cursor?: number, replyType?: number): Promise<[Notice[], Record<string, unknown>, boolean]>;
13
+ ats(cursor?: number): Promise<[Notice[], Record<string, unknown>, boolean]>;
14
+ likes(cursor?: number, likeType?: number): Promise<[Notice[], Record<string, unknown>, boolean]>;
15
+ private fetch;
16
+ private static parse;
17
+ }
@@ -0,0 +1,54 @@
1
+ /** 消息中心:未读数、回复我的、@我的、收到的赞。
2
+
3
+ 注意:这类列表接口的分页游标(cursor.id)实测传回去仍返回同一页,
4
+ 所以这里只保证「最近一页」;更早的消息请到网页端查看。
5
+ */
6
+ import * as endpoints from "../endpoints";
7
+ import { Notice } from "../models";
8
+ const COMMON_PARAMS = { platform: "web", build: 0, mobi_app: "web" };
9
+ function cursorId(cursor) {
10
+ if (cursor && typeof cursor === "object")
11
+ return Number(cursor.id ?? 0);
12
+ return Number(cursor ?? 0);
13
+ }
14
+ export class NotifyAPI {
15
+ client;
16
+ constructor(client) {
17
+ this.client = client;
18
+ }
19
+ async unread() {
20
+ const payload = await this.client.get(endpoints.MSGFEED_UNREAD);
21
+ return payload?.data ?? {};
22
+ }
23
+ async replies(cursor = 0, replyType = 0) {
24
+ const data = this.fetch(endpoints.MSGFEED_REPLY, { id: cursorId(cursor), reply_type: replyType });
25
+ return NotifyAPI.parse(await data, Notice.fromReply);
26
+ }
27
+ async ats(cursor = 0) {
28
+ const data = this.fetch(endpoints.MSGFEED_AT, { id: cursorId(cursor) });
29
+ return NotifyAPI.parse(await data, Notice.fromAt);
30
+ }
31
+ async likes(cursor = 0, likeType = 0) {
32
+ const data = await this.fetch(endpoints.MSGFEED_LIKE, {
33
+ id: cursorId(cursor),
34
+ like_type: likeType,
35
+ });
36
+ const block = (await data).total ?? {};
37
+ const items = (block.items ?? [])
38
+ .filter((i) => i && typeof i === "object")
39
+ .map((i) => Notice.fromLike(i));
40
+ const cursorData = block.cursor ?? {};
41
+ return [items, cursorData, Boolean(cursorData.is_end)];
42
+ }
43
+ async fetch(url, params) {
44
+ const payload = await this.client.get(url, { ...COMMON_PARAMS, ...params });
45
+ return payload?.data ?? {};
46
+ }
47
+ static parse(data, parser) {
48
+ const items = (data.items ?? [])
49
+ .filter((i) => i && typeof i === "object")
50
+ .map((i) => parser(i));
51
+ const cursor = data.cursor ?? {};
52
+ return [items, cursor, Boolean(cursor.is_end)];
53
+ }
54
+ }
@@ -0,0 +1,15 @@
1
+ /** 个人资料:读取、修改签名、修改昵称 / 性别 / 生日。
2
+
3
+ 写接口都没有在真实账号上执行过:
4
+ - updateSign 用的是 x/member/web/sign/update,改完进入审核队列。
5
+ - updateProfile 用的是 x/member/web/update,只支持昵称、性别、生日。
6
+ */
7
+ import type { BiliClient } from "../client";
8
+ export declare class ProfileAPI {
9
+ private client;
10
+ constructor(client: BiliClient);
11
+ info(): Promise<Record<string, unknown>>;
12
+ avatarUrl(): Promise<string>;
13
+ updateSign(sign: string): Promise<void>;
14
+ updateProfile(uname?: string, sex?: string, birthday?: string): Promise<void>;
15
+ }
@@ -0,0 +1,59 @@
1
+ /** 个人资料:读取、修改签名、修改昵称 / 性别 / 生日。
2
+
3
+ 写接口都没有在真实账号上执行过:
4
+ - updateSign 用的是 x/member/web/sign/update,改完进入审核队列。
5
+ - updateProfile 用的是 x/member/web/update,只支持昵称、性别、生日。
6
+ */
7
+ import * as endpoints from "../endpoints";
8
+ const SEX_VALUES = ["男", "女", "保密"];
9
+ const MAX_SIGN_LENGTH = 70;
10
+ export class ProfileAPI {
11
+ client;
12
+ constructor(client) {
13
+ this.client = client;
14
+ }
15
+ async info() {
16
+ const payload = await this.client.get(endpoints.MEMBER_ACCOUNT);
17
+ return payload?.data ?? {};
18
+ }
19
+ async avatarUrl() {
20
+ const payload = await this.client.get(endpoints.MEMBER_MYINFO);
21
+ return String((payload?.data ?? {}).face ?? "");
22
+ }
23
+ async updateSign(sign) {
24
+ sign = (sign || "").trim();
25
+ if (sign.length > MAX_SIGN_LENGTH) {
26
+ throw new Error(`签名最多 ${MAX_SIGN_LENGTH} 个字符`);
27
+ }
28
+ await this.client.post(endpoints.MEMBER_SIGN_UPDATE, {
29
+ user_sign: sign,
30
+ csrf: this.client.csrf,
31
+ });
32
+ }
33
+ async updateProfile(uname, sex, birthday) {
34
+ const form = { csrf: this.client.csrf };
35
+ if (uname != null) {
36
+ const name = uname.trim();
37
+ if (!(1 <= name.length && name.length <= 20)) {
38
+ throw new Error("昵称长度需要在 1~20 个字符之间");
39
+ }
40
+ form["uname"] = name;
41
+ }
42
+ if (sex != null) {
43
+ if (!SEX_VALUES.includes(sex)) {
44
+ throw new Error(`sex 只支持 ${SEX_VALUES}`);
45
+ }
46
+ form["sex"] = sex;
47
+ }
48
+ if (birthday != null) {
49
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(birthday)) {
50
+ throw new Error("birthday 需要 YYYY-MM-DD 格式");
51
+ }
52
+ form["birthday"] = birthday;
53
+ }
54
+ if (Object.keys(form).length === 1) {
55
+ throw new Error("至少要修改一项:uname / sex / birthday");
56
+ }
57
+ await this.client.post(endpoints.MEMBER_PROFILE_UPDATE, form);
58
+ }
59
+ }