koishi-plugin-bilibili-notify-dynamic 0.0.1
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.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -0
- package/lib/index.cjs +292 -0
- package/lib/index.d.cts +295 -0
- package/package.json +60 -0
- package/src/config.ts +71 -0
- package/src/dynamic-filter.ts +85 -0
- package/src/dynamic-service.ts +299 -0
- package/src/index.ts +21 -0
- package/src/types.ts +111 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +10 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# koishi-plugin-bilibili-notify-dynamic
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-bilibili-notify-dynamic)
|
|
4
|
+
|
|
5
|
+
`koishi-plugin-bilibili-notify` 的动态推送插件,通过定时轮询获取 B 站 UP 主最新动态。
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> 需要先安装并配置 `koishi-plugin-bilibili-notify` 核心插件
|
|
9
|
+
|
|
10
|
+
## 功能
|
|
11
|
+
|
|
12
|
+
- 定时轮询 UP 主动态(cron 表达式可配置,默认每 2 分钟)
|
|
13
|
+
- 支持推送图文、视频、专栏、转发等各类动态
|
|
14
|
+
- 可配置动态屏蔽规则(关键词、正则、白名单)
|
|
15
|
+
- 可选生成动态卡片图片(需安装 `koishi-plugin-bilibili-notify-image`)
|
|
16
|
+
- 视频动态支持附带 BV 号链接
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
在 Koishi 插件市场中搜索 `bilibili-notify-dynamic` 并安装。
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
MIT
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let koishi = require("koishi");
|
|
3
|
+
let _bilibili_notify_internal = require("@bilibili-notify/internal");
|
|
4
|
+
let _bilibili_notify_push = require("@bilibili-notify/push");
|
|
5
|
+
let cron = require("cron");
|
|
6
|
+
require("koishi-plugin-bilibili-notify");
|
|
7
|
+
let luxon = require("luxon");
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
const BilibiliNotifyDynamicSchema = koishi.Schema.object({
|
|
10
|
+
logLevel: koishi.Schema.number().min(1).max(3).step(1).default(1).description("这里可以设置日志等级喔~3 是最详细的调试信息,1 是只显示错误信息。主人可以根据需要选择合适的等级,让女仆更好地为您服务 (๑•̀ㅂ•́)و✧"),
|
|
11
|
+
dynamicUrl: koishi.Schema.boolean().default(false).description("发送动态时要不要顺便发链接呢?但如果主人用的是 QQ 官方机器人,这个开关不要开喔~不然会出事的 (;>_<)!"),
|
|
12
|
+
dynamicCron: koishi.Schema.string().default("*/2 * * * *").description("主人想多久检查一次动态呢?这里填写 cron 表达式~太短太频繁会吓到女仆的,请温柔一点 (〃ノωノ)"),
|
|
13
|
+
dynamicVideoUrlToBV: koishi.Schema.boolean().default(false).description("如果是视频动态,开启后会把链接换成 BV 号哦~方便主人的其他用途 (*´・ω・`)"),
|
|
14
|
+
pushImgsInDynamic: koishi.Schema.boolean().default(false).description("要不要把动态里的图片也一起推送呢?但、但是可能会触发 QQ 的风控,女仆会有点害怕 (;>_<) 请主人小心决定…"),
|
|
15
|
+
filter: koishi.Schema.intersect([koishi.Schema.object({ enable: koishi.Schema.boolean().default(false).description("要开启吗?") }).description("这里是动态屏蔽设置~如果有不想看到的内容,女仆可以帮主人过滤掉 (>﹏<)!"), koishi.Schema.union([koishi.Schema.object({
|
|
16
|
+
enable: koishi.Schema.const(true).required(),
|
|
17
|
+
notify: koishi.Schema.boolean().default(false).description("当动态被屏蔽时,要不要让女仆通知主人呢?"),
|
|
18
|
+
regex: koishi.Schema.string().description("这里可以填写正则表达式,用来屏蔽特定动态~女仆会努力匹配的!"),
|
|
19
|
+
keywords: koishi.Schema.array(String).description("这里填写关键字,每一个都是单独的一项~有这些词的动态女仆都会贴心地拦下来 (*´∀`)"),
|
|
20
|
+
forward: koishi.Schema.boolean().default(false).description("要不要屏蔽转发动态呢?主人说了算!"),
|
|
21
|
+
article: koishi.Schema.boolean().default(false).description("是否屏蔽专栏动态~女仆会按照主人的喜好来处理 (๑•̀ㅂ•́)و✧"),
|
|
22
|
+
whitelistEnable: koishi.Schema.boolean().default(false).description("是否启用白名单过滤(仅推送匹配白名单规则的动态)"),
|
|
23
|
+
whitelistRegex: koishi.Schema.string().description("白名单正则表达式,命中时允许推送该动态"),
|
|
24
|
+
whitelistKeywords: koishi.Schema.array(String).description("白名单关键词,命中任意关键词时允许推送该动态")
|
|
25
|
+
}), koishi.Schema.object({})])])
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/types.ts
|
|
29
|
+
let DynamicFilterReason = /* @__PURE__ */ function(DynamicFilterReason) {
|
|
30
|
+
DynamicFilterReason["BlacklistKeyword"] = "blacklist-keyword";
|
|
31
|
+
DynamicFilterReason["BlacklistForward"] = "blacklist-forward";
|
|
32
|
+
DynamicFilterReason["BlacklistArticle"] = "blacklist-article";
|
|
33
|
+
DynamicFilterReason["WhitelistUnmatched"] = "whitelist-unmatched";
|
|
34
|
+
return DynamicFilterReason;
|
|
35
|
+
}({});
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/dynamic-filter.ts
|
|
38
|
+
function collectRichText(dynamic, texts) {
|
|
39
|
+
const richTextNodes = dynamic.modules?.module_dynamic?.desc?.rich_text_nodes;
|
|
40
|
+
if (richTextNodes?.length) texts.push(richTextNodes.map((n) => n.text ?? "").join(""));
|
|
41
|
+
const summaryNodes = dynamic.modules?.module_dynamic?.major?.opus?.summary?.rich_text_nodes;
|
|
42
|
+
if (summaryNodes?.length) texts.push(summaryNodes.map((n) => n.text ?? "").join(""));
|
|
43
|
+
const title = dynamic.modules?.module_dynamic?.major?.opus?.title;
|
|
44
|
+
if (title) texts.push(title);
|
|
45
|
+
const archiveTitle = dynamic.modules?.module_dynamic?.major?.archive?.title;
|
|
46
|
+
if (archiveTitle) texts.push(archiveTitle);
|
|
47
|
+
}
|
|
48
|
+
function getDynamicText(dynamic) {
|
|
49
|
+
const texts = [];
|
|
50
|
+
collectRichText(dynamic, texts);
|
|
51
|
+
if (dynamic.orig) collectRichText(dynamic.orig, texts);
|
|
52
|
+
return texts.join("\n");
|
|
53
|
+
}
|
|
54
|
+
function safeRegexTest(pattern, text) {
|
|
55
|
+
if (!pattern) return false;
|
|
56
|
+
try {
|
|
57
|
+
return new RegExp(pattern).test(text);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn(`[bilibili-notify-dynamic] 无效的正则表达式 "${pattern}": ${e.message}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function testKeywordMatched(text, keywords) {
|
|
64
|
+
if (!keywords?.length) return false;
|
|
65
|
+
return keywords.some((kw) => kw && text.includes(kw));
|
|
66
|
+
}
|
|
67
|
+
function filterDynamic(dynamic, config) {
|
|
68
|
+
const cfg = {
|
|
69
|
+
enable: false,
|
|
70
|
+
regex: "",
|
|
71
|
+
keywords: [],
|
|
72
|
+
forward: false,
|
|
73
|
+
article: false,
|
|
74
|
+
whitelistEnable: false,
|
|
75
|
+
whitelistRegex: "",
|
|
76
|
+
whitelistKeywords: [],
|
|
77
|
+
...config
|
|
78
|
+
};
|
|
79
|
+
const text = getDynamicText(dynamic);
|
|
80
|
+
if (cfg.enable) {
|
|
81
|
+
if (cfg.forward && dynamic.type === "DYNAMIC_TYPE_FORWARD") return {
|
|
82
|
+
blocked: true,
|
|
83
|
+
reason: DynamicFilterReason.BlacklistForward
|
|
84
|
+
};
|
|
85
|
+
if (cfg.article && dynamic.type === "DYNAMIC_TYPE_ARTICLE") return {
|
|
86
|
+
blocked: true,
|
|
87
|
+
reason: DynamicFilterReason.BlacklistArticle
|
|
88
|
+
};
|
|
89
|
+
if (safeRegexTest(cfg.regex, text) || testKeywordMatched(text, cfg.keywords)) return {
|
|
90
|
+
blocked: true,
|
|
91
|
+
reason: DynamicFilterReason.BlacklistKeyword
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (cfg.whitelistEnable) {
|
|
95
|
+
if ((!!cfg.whitelistRegex || cfg.whitelistKeywords.length > 0) && !safeRegexTest(cfg.whitelistRegex, text) && !testKeywordMatched(text, cfg.whitelistKeywords)) return {
|
|
96
|
+
blocked: true,
|
|
97
|
+
reason: DynamicFilterReason.WhitelistUnmatched
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { blocked: false };
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/dynamic-service.ts
|
|
104
|
+
const SERVICE_NAME = "bilibili-notify-dynamic";
|
|
105
|
+
/** Simple async lock: if the previous run is still executing, skip. */
|
|
106
|
+
function withLock(fn) {
|
|
107
|
+
let locked = false;
|
|
108
|
+
return () => {
|
|
109
|
+
if (locked) return;
|
|
110
|
+
locked = true;
|
|
111
|
+
fn().catch((err) => {
|
|
112
|
+
console.error("[bilibili-notify-dynamic] Execution error:", err);
|
|
113
|
+
}).finally(() => {
|
|
114
|
+
locked = false;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
var BilibiliNotifyDynamic = class extends koishi.Service {
|
|
119
|
+
static [koishi.Service.provide] = SERVICE_NAME;
|
|
120
|
+
dynamicLogger;
|
|
121
|
+
api;
|
|
122
|
+
push;
|
|
123
|
+
dynamicJob;
|
|
124
|
+
dynamicSubManager = /* @__PURE__ */ new Map();
|
|
125
|
+
dynamicTimelineManager = /* @__PURE__ */ new Map();
|
|
126
|
+
constructor(ctx, config) {
|
|
127
|
+
super(ctx, SERVICE_NAME);
|
|
128
|
+
this.config = config;
|
|
129
|
+
this.dynamicLogger = new koishi.Logger(SERVICE_NAME);
|
|
130
|
+
this.dynamicLogger.level = config.logLevel;
|
|
131
|
+
}
|
|
132
|
+
start() {
|
|
133
|
+
const internals = this.ctx["bilibili-notify"].getInternals(_bilibili_notify_internal.BILIBILI_NOTIFY_TOKEN);
|
|
134
|
+
if (!internals) throw new Error("无法获取 bilibili-notify 内部实例,请确认核心插件已启动");
|
|
135
|
+
this.api = internals.api;
|
|
136
|
+
this.push = internals.push;
|
|
137
|
+
this.dynamicTimelineManager = /* @__PURE__ */ new Map();
|
|
138
|
+
if (internals.subs) this.startDynamicDetector(internals.subs);
|
|
139
|
+
this.ctx.on("bilibili-notify/subscription-changed", (subs) => {
|
|
140
|
+
this.startDynamicDetector(subs);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
stop() {
|
|
144
|
+
if (this.dynamicJob) {
|
|
145
|
+
this.dynamicJob.stop();
|
|
146
|
+
this.dynamicLogger.info("动态检测任务已停止");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
get isActive() {
|
|
150
|
+
return this.dynamicJob?.running ?? false;
|
|
151
|
+
}
|
|
152
|
+
startDynamicDetector(subs) {
|
|
153
|
+
if (this.dynamicJob) {
|
|
154
|
+
this.dynamicJob.stop();
|
|
155
|
+
this.dynamicJob = void 0;
|
|
156
|
+
}
|
|
157
|
+
const dynamicSubManager = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const sub of Object.values(subs)) if (sub.dynamic) {
|
|
159
|
+
if (!this.dynamicTimelineManager.has(sub.uid)) this.dynamicTimelineManager.set(sub.uid, Math.floor(luxon.DateTime.now().toSeconds()));
|
|
160
|
+
dynamicSubManager.set(sub.uid, sub);
|
|
161
|
+
}
|
|
162
|
+
for (const uid of this.dynamicTimelineManager.keys()) if (!dynamicSubManager.has(uid)) this.dynamicTimelineManager.delete(uid);
|
|
163
|
+
if (dynamicSubManager.size === 0) {
|
|
164
|
+
this.dynamicLogger.info("没有需要动态检测的订阅对象");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.dynamicSubManager = dynamicSubManager;
|
|
168
|
+
this.dynamicJob = new cron.CronJob(this.config.dynamicCron, withLock(() => this.detectDynamics()));
|
|
169
|
+
this.dynamicJob.start();
|
|
170
|
+
this.dynamicLogger.info("动态检测任务已启动");
|
|
171
|
+
}
|
|
172
|
+
async detectDynamics() {
|
|
173
|
+
this.dynamicLogger.debug("开始获取动态信息");
|
|
174
|
+
let content;
|
|
175
|
+
try {
|
|
176
|
+
content = await this.api.getAllDynamic();
|
|
177
|
+
} catch (e) {
|
|
178
|
+
this.dynamicLogger.error(`获取动态失败:${e}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!content) return;
|
|
182
|
+
if (content.code !== 0) {
|
|
183
|
+
await this.handleApiError(content.code, content.message);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.dynamicLogger.debug("成功获取动态信息,开始处理");
|
|
187
|
+
const currentPushDyn = {};
|
|
188
|
+
for (const item of content.data.items) {
|
|
189
|
+
if (!item) continue;
|
|
190
|
+
const postTime = item.modules.module_author.pub_ts;
|
|
191
|
+
if (typeof postTime !== "number" || !Number.isFinite(postTime)) {
|
|
192
|
+
this.dynamicLogger.warn(`跳过无效动态:pub_ts 缺失或非数字,ID=${item.id_str ?? "unknown"}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const uid = item.modules.module_author.mid.toString();
|
|
196
|
+
const name = item.modules.module_author.name;
|
|
197
|
+
const timeline = this.dynamicTimelineManager.get(uid);
|
|
198
|
+
if (timeline === void 0) continue;
|
|
199
|
+
this.dynamicLogger.debug(`检查动态 UP=${name} UID=${uid} 发布时间=${luxon.DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`);
|
|
200
|
+
if (timeline >= postTime) continue;
|
|
201
|
+
const filterResult = filterDynamic(item, this.config.filter ?? {});
|
|
202
|
+
if (filterResult.blocked) {
|
|
203
|
+
if (this.config.filter?.notify) {
|
|
204
|
+
const msgs = {
|
|
205
|
+
[DynamicFilterReason.BlacklistKeyword]: `${name}发布了一条含有屏蔽关键字的动态`,
|
|
206
|
+
[DynamicFilterReason.BlacklistForward]: `${name}转发了一条动态,已屏蔽`,
|
|
207
|
+
[DynamicFilterReason.BlacklistArticle]: `${name}投稿了一条专栏,已屏蔽`,
|
|
208
|
+
[DynamicFilterReason.WhitelistUnmatched]: `${name}发布了一条不在白名单范围内的动态,已屏蔽`
|
|
209
|
+
};
|
|
210
|
+
await this.push.broadcastToTargets(uid, (0, koishi.h)("message", msgs[filterResult.reason]), _bilibili_notify_push.PushType.Dynamic);
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const sub = this.dynamicSubManager.get(uid);
|
|
215
|
+
const imageService = this.ctx["bilibili-notify-image"];
|
|
216
|
+
let buffer;
|
|
217
|
+
try {
|
|
218
|
+
if (imageService?.generateDynamicCard) buffer = await imageService.generateDynamicCard(item, sub?.customCardStyle?.enable ? sub.customCardStyle : void 0);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
const err = e;
|
|
221
|
+
if (err.message === "直播开播动态,不做处理") continue;
|
|
222
|
+
this.dynamicLogger.error(`生成动态图片失败:${err.message},动态检测已停止`);
|
|
223
|
+
await this.push.sendErrorMsg(`生成动态图片失败:${err.message},动态检测已停止`);
|
|
224
|
+
this.dynamicJob?.stop();
|
|
225
|
+
this.dynamicJob = void 0;
|
|
226
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, `生成动态图片失败:${err.message}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let dUrl = "";
|
|
230
|
+
if (this.config.dynamicUrl) if (item.type === "DYNAMIC_TYPE_AV") {
|
|
231
|
+
const jumpUrl = item.modules.module_dynamic.major?.archive?.jump_url ?? "";
|
|
232
|
+
if (this.config.dynamicVideoUrlToBV) {
|
|
233
|
+
const bvMatch = jumpUrl.match(/BV[0-9A-Za-z]+/);
|
|
234
|
+
dUrl = bvMatch ? bvMatch[0] : "";
|
|
235
|
+
} else dUrl = `${name}发布了新视频:https:${jumpUrl}`;
|
|
236
|
+
} else dUrl = `${name}发布了一条动态:https://t.bilibili.com/${item.id_str}`;
|
|
237
|
+
const msgContent = buffer ? [koishi.h.image(buffer, "image/jpeg"), koishi.h.text(dUrl)] : [koishi.h.text(`${name}发布了一条动态${dUrl ? `:${dUrl}` : ""}`)];
|
|
238
|
+
await this.push.broadcastToTargets(uid, (0, koishi.h)("message", msgContent), _bilibili_notify_push.PushType.Dynamic);
|
|
239
|
+
if (this.config.pushImgsInDynamic && item.type === "DYNAMIC_TYPE_DRAW") {
|
|
240
|
+
const pics = item.modules?.module_dynamic?.major?.opus?.pics;
|
|
241
|
+
if (pics?.length) {
|
|
242
|
+
const picsMsg = (0, koishi.h)("message", { forward: true }, pics.map((p) => koishi.h.img(p.url)));
|
|
243
|
+
await this.push.broadcastToTargets(uid, picsMsg, _bilibili_notify_push.PushType.Dynamic);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!currentPushDyn[uid]) currentPushDyn[uid] = item;
|
|
247
|
+
}
|
|
248
|
+
for (const [uid, item] of Object.entries(currentPushDyn)) {
|
|
249
|
+
const postTime = item.modules.module_author.pub_ts;
|
|
250
|
+
this.dynamicTimelineManager.set(uid, postTime);
|
|
251
|
+
this.dynamicLogger.debug(`更新时间线 UID=${uid} 时间=${luxon.DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`);
|
|
252
|
+
}
|
|
253
|
+
this.dynamicLogger.debug(`本次推送 ${Object.keys(currentPushDyn).length} 条动态`);
|
|
254
|
+
}
|
|
255
|
+
async handleApiError(code, message) {
|
|
256
|
+
this.dynamicJob?.stop();
|
|
257
|
+
this.dynamicJob = void 0;
|
|
258
|
+
switch (code) {
|
|
259
|
+
case -101:
|
|
260
|
+
this.dynamicLogger.error("账号未登录,动态检测已停止");
|
|
261
|
+
await this.push.sendPrivateMsg("账号未登录,请先登录");
|
|
262
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, "账号未登录");
|
|
263
|
+
break;
|
|
264
|
+
case -352:
|
|
265
|
+
this.dynamicLogger.error("账号被风控,动态检测已停止");
|
|
266
|
+
await this.push.sendPrivateMsg("账号被风控,请使用 `bili cap` 指令解除风控");
|
|
267
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, "账号被风控");
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
this.dynamicLogger.error(`获取动态信息失败,错误码:${code},${message}`);
|
|
271
|
+
await this.push.sendPrivateMsg(`获取动态信息失败,错误码:${code}`);
|
|
272
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, `获取动态失败,错误码:${code}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/index.ts
|
|
278
|
+
const name = "bilibili-notify-dynamic";
|
|
279
|
+
const inject = {
|
|
280
|
+
required: ["bilibili-notify"],
|
|
281
|
+
optional: ["bilibili-notify-image"]
|
|
282
|
+
};
|
|
283
|
+
const Config = BilibiliNotifyDynamicSchema;
|
|
284
|
+
function apply(ctx, config) {
|
|
285
|
+
ctx.plugin(BilibiliNotifyDynamic, config);
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
exports.BilibiliNotifyDynamic = BilibiliNotifyDynamic;
|
|
289
|
+
exports.Config = Config;
|
|
290
|
+
exports.apply = apply;
|
|
291
|
+
exports.inject = inject;
|
|
292
|
+
exports.name = name;
|
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { Awaitable, Context, Service } from "koishi";
|
|
2
|
+
import { Subscriptions } from "@bilibili-notify/push";
|
|
3
|
+
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region ../../node_modules/cosmokit/lib/index.d.ts
|
|
7
|
+
type Dict<T = any, K extends string | symbol = string> = { [key in K]: T };
|
|
8
|
+
declare function isArrayBufferLike(value: any): value is ArrayBufferLike;
|
|
9
|
+
declare function isArrayBufferSource(value: any): value is Binary.Source;
|
|
10
|
+
declare namespace Binary {
|
|
11
|
+
type Source<T extends ArrayBufferLike = ArrayBufferLike> = T | ArrayBufferView<T>;
|
|
12
|
+
const is: typeof isArrayBufferLike;
|
|
13
|
+
const isSource: typeof isArrayBufferSource;
|
|
14
|
+
function fromSource<T extends ArrayBufferLike>(source: Source<T>): T;
|
|
15
|
+
function toBase64(source: Source): string;
|
|
16
|
+
function fromBase64(source: string): ArrayBuffer | Uint8Array<ArrayBuffer>;
|
|
17
|
+
function toHex(source: Source): string;
|
|
18
|
+
function fromHex(source: string): ArrayBuffer;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region ../../node_modules/@standard-schema/spec/dist/index.d.ts
|
|
22
|
+
/** The Standard Typed interface. This is a base type extended by other specs. */
|
|
23
|
+
interface StandardTypedV1<Input = unknown, Output = Input> {
|
|
24
|
+
/** The Standard properties. */
|
|
25
|
+
readonly "~standard": StandardTypedV1.Props<Input, Output>;
|
|
26
|
+
}
|
|
27
|
+
declare namespace StandardTypedV1 {
|
|
28
|
+
/** The Standard Typed properties interface. */
|
|
29
|
+
interface Props<Input = unknown, Output = Input> {
|
|
30
|
+
/** The version number of the standard. */
|
|
31
|
+
readonly version: 1;
|
|
32
|
+
/** The vendor name of the schema library. */
|
|
33
|
+
readonly vendor: string;
|
|
34
|
+
/** Inferred types associated with the schema. */
|
|
35
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The Standard Typed types interface. */
|
|
38
|
+
interface Types<Input = unknown, Output = Input> {
|
|
39
|
+
/** The input type of the schema. */
|
|
40
|
+
readonly input: Input;
|
|
41
|
+
/** The output type of the schema. */
|
|
42
|
+
readonly output: Output;
|
|
43
|
+
}
|
|
44
|
+
/** Infers the input type of a Standard Typed. */
|
|
45
|
+
type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
46
|
+
/** Infers the output type of a Standard Typed. */
|
|
47
|
+
type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
48
|
+
}
|
|
49
|
+
/** The Standard Schema interface. */
|
|
50
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
51
|
+
/** The Standard Schema properties. */
|
|
52
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
53
|
+
}
|
|
54
|
+
declare namespace StandardSchemaV1 {
|
|
55
|
+
/** The Standard Schema properties interface. */
|
|
56
|
+
interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
|
|
57
|
+
/** Validates unknown input values. */
|
|
58
|
+
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
|
|
59
|
+
}
|
|
60
|
+
/** The result interface of the validate function. */
|
|
61
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
62
|
+
/** The result interface if validation succeeds. */
|
|
63
|
+
interface SuccessResult<Output> {
|
|
64
|
+
/** The typed output value. */
|
|
65
|
+
readonly value: Output;
|
|
66
|
+
/** A falsy value for `issues` indicates success. */
|
|
67
|
+
readonly issues?: undefined;
|
|
68
|
+
}
|
|
69
|
+
interface Options {
|
|
70
|
+
/** Explicit support for additional vendor-specific parameters, if needed. */
|
|
71
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
72
|
+
}
|
|
73
|
+
/** The result interface if validation fails. */
|
|
74
|
+
interface FailureResult {
|
|
75
|
+
/** The issues of failed validation. */
|
|
76
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
77
|
+
}
|
|
78
|
+
/** The issue interface of the failure output. */
|
|
79
|
+
interface Issue {
|
|
80
|
+
/** The error message of the issue. */
|
|
81
|
+
readonly message: string;
|
|
82
|
+
/** The path of the issue, if any. */
|
|
83
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
84
|
+
}
|
|
85
|
+
/** The path segment interface of the issue. */
|
|
86
|
+
interface PathSegment {
|
|
87
|
+
/** The key representing a path segment. */
|
|
88
|
+
readonly key: PropertyKey;
|
|
89
|
+
}
|
|
90
|
+
/** The Standard types interface. */
|
|
91
|
+
interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
|
|
92
|
+
/** Infers the input type of a Standard. */
|
|
93
|
+
type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
|
|
94
|
+
/** Infers the output type of a Standard. */
|
|
95
|
+
type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
|
|
96
|
+
}
|
|
97
|
+
/** The Standard JSON Schema interface. */
|
|
98
|
+
declare namespace index_d_exports {
|
|
99
|
+
export { Schema as default };
|
|
100
|
+
}
|
|
101
|
+
declare const kSchema: unique symbol;
|
|
102
|
+
declare global {
|
|
103
|
+
namespace Schemastery {
|
|
104
|
+
type From<X> = X extends string | number | boolean ? Schema<X> : X extends Schema ? X : X extends typeof String ? Schema<string> : X extends typeof Number ? Schema<number> : X extends typeof Boolean ? Schema<boolean> : X extends typeof Function ? Schema<Function, (...args: any[]) => any> : X extends Constructor<infer S> ? Schema<S> : never;
|
|
105
|
+
type TypeS1<X> = X extends Schema<infer S, unknown> ? S : never;
|
|
106
|
+
type Inverse<X> = X extends Schema<any, infer Y> ? (arg: Y) => void : never;
|
|
107
|
+
type TypeS<X> = TypeS1<From<X>>;
|
|
108
|
+
type TypeT<X> = ReturnType<From<X>>;
|
|
109
|
+
type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?];
|
|
110
|
+
type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never;
|
|
111
|
+
type IntersectT<X> = Inverse<From<X>> extends ((arg: infer T) => void) ? T : never;
|
|
112
|
+
type TupleS<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeS<L>?, ...TupleS<R>] : any[];
|
|
113
|
+
type TupleT<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeT<L>?, ...TupleT<R>] : any[];
|
|
114
|
+
type ObjectS<X extends Dict> = { [K in keyof X]?: TypeS<X[K]> | null } & Dict;
|
|
115
|
+
type ObjectT<X extends Dict> = { [K in keyof X]: TypeT<X[K]> } & Dict;
|
|
116
|
+
type Constructor<T = any> = new (...args: any[]) => T;
|
|
117
|
+
interface Static {
|
|
118
|
+
<T = any>(options: Partial<Schema<T>>): Schema<T>;
|
|
119
|
+
new <T = any>(options: Partial<Schema<T>>): Schema<T>;
|
|
120
|
+
prototype: Schema;
|
|
121
|
+
resolve: Resolve;
|
|
122
|
+
from<X = any>(source?: X): From<X>;
|
|
123
|
+
extend(type: string, resolve: Resolve): void;
|
|
124
|
+
any<T = any>(): Schema<T>;
|
|
125
|
+
never(): Schema<never>;
|
|
126
|
+
const<const T>(value: T): Schema<T>;
|
|
127
|
+
string(): Schema<string>;
|
|
128
|
+
number(): Schema<number>;
|
|
129
|
+
natural(): Schema<number>;
|
|
130
|
+
percent(): Schema<number>;
|
|
131
|
+
boolean(): Schema<boolean>;
|
|
132
|
+
date(): Schema<string | Date, Date>;
|
|
133
|
+
regExp(flag?: string): Schema<string | RegExp, RegExp>;
|
|
134
|
+
arrayBuffer(): Schema<Binary.Source, ArrayBufferLike>;
|
|
135
|
+
arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike>;
|
|
136
|
+
bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number>;
|
|
137
|
+
function(): Schema<Function, (...args: any[]) => any>;
|
|
138
|
+
is(constructor: string): Schema;
|
|
139
|
+
is<T>(constructor: Constructor<T>): Schema<T>;
|
|
140
|
+
array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>;
|
|
141
|
+
dict<X, Y extends Schema<any, string> = Schema<string>>(inner: X, sKey?: Y): Schema<Dict<TypeS<X>, TypeS<Y>>, Dict<TypeT<X>, TypeT<Y>>>;
|
|
142
|
+
tuple<const X extends readonly any[]>(list: X): Schema<TupleS<X>, TupleT<X>>;
|
|
143
|
+
object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>;
|
|
144
|
+
union<const X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>;
|
|
145
|
+
intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>;
|
|
146
|
+
transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T>;
|
|
147
|
+
lazy<X extends Schema>(callback: () => X): X;
|
|
148
|
+
ValidationError: typeof ValidationError;
|
|
149
|
+
}
|
|
150
|
+
interface Options {
|
|
151
|
+
autofix?: boolean;
|
|
152
|
+
ignore?(data: any, schema: Schema): boolean;
|
|
153
|
+
path?: (keyof any)[];
|
|
154
|
+
}
|
|
155
|
+
interface Meta<T = any> {
|
|
156
|
+
default?: T extends {} ? Partial<T> : T;
|
|
157
|
+
required?: boolean;
|
|
158
|
+
disabled?: boolean;
|
|
159
|
+
collapse?: boolean;
|
|
160
|
+
badges?: {
|
|
161
|
+
text: string;
|
|
162
|
+
type: string;
|
|
163
|
+
}[];
|
|
164
|
+
hidden?: boolean;
|
|
165
|
+
loose?: boolean;
|
|
166
|
+
role?: string;
|
|
167
|
+
extra?: any;
|
|
168
|
+
link?: string;
|
|
169
|
+
description?: string | Dict<string>;
|
|
170
|
+
comment?: string;
|
|
171
|
+
pattern?: {
|
|
172
|
+
source: string;
|
|
173
|
+
flags?: string;
|
|
174
|
+
};
|
|
175
|
+
max?: number;
|
|
176
|
+
min?: number;
|
|
177
|
+
step?: number;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
interface Schemastery<S = any, T = S> {
|
|
181
|
+
(data?: S | null, options?: Schemastery.Options): T;
|
|
182
|
+
new (data?: S | null, options?: Schemastery.Options): T;
|
|
183
|
+
[kSchema]: true;
|
|
184
|
+
uid: number;
|
|
185
|
+
meta: Schemastery.Meta<T>;
|
|
186
|
+
type: string;
|
|
187
|
+
sKey?: Schema;
|
|
188
|
+
inner?: Schema;
|
|
189
|
+
list?: Schema[];
|
|
190
|
+
dict?: Dict<Schema>;
|
|
191
|
+
bits?: Dict<number>;
|
|
192
|
+
callback?: Function;
|
|
193
|
+
constructor?: string | Function;
|
|
194
|
+
builder?: Function;
|
|
195
|
+
value?: T;
|
|
196
|
+
refs?: Dict<Schema>;
|
|
197
|
+
preserve?: boolean;
|
|
198
|
+
'~standard': StandardSchemaV1.Props;
|
|
199
|
+
toString(inline?: boolean): string;
|
|
200
|
+
toJSON(): Schema<S, T>;
|
|
201
|
+
required(value?: boolean): Schema<S, T>;
|
|
202
|
+
hidden(value?: boolean): Schema<S, T>;
|
|
203
|
+
loose(value?: boolean): Schema<S, T>;
|
|
204
|
+
role(text: string, extra?: any): Schema<S, T>;
|
|
205
|
+
link(link: string): Schema<S, T>;
|
|
206
|
+
default(value: T): Schema<S, T>;
|
|
207
|
+
comment(text: string): Schema<S, T>;
|
|
208
|
+
description(text: string): Schema<S, T>;
|
|
209
|
+
disabled(value?: boolean): Schema<S, T>;
|
|
210
|
+
collapse(value?: boolean): Schema<S, T>;
|
|
211
|
+
deprecated(): Schema<S, T>;
|
|
212
|
+
experimental(): Schema<S, T>;
|
|
213
|
+
pattern(regexp: RegExp): Schema<S, T>;
|
|
214
|
+
max(value: number): Schema<S, T>;
|
|
215
|
+
min(value: number): Schema<S, T>;
|
|
216
|
+
step(value: number): Schema<S, T>;
|
|
217
|
+
set(key: string, value: Schema): Schema<S, T>;
|
|
218
|
+
push(value: Schema): Schema<S, T>;
|
|
219
|
+
simplify(value?: any): any;
|
|
220
|
+
i18n(messages: Dict): Schema<S, T>;
|
|
221
|
+
extra<K extends keyof Schemastery.Meta>(key: K, value: Schemastery.Meta[K]): Schema<S, T>;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
declare class ValidationError extends TypeError {
|
|
225
|
+
options: Schemastery.Options;
|
|
226
|
+
name: string;
|
|
227
|
+
constructor(message: string, options: Schemastery.Options);
|
|
228
|
+
static is(error: any): error is ValidationError;
|
|
229
|
+
}
|
|
230
|
+
type Schema<S = any, T = S> = Schemastery<S, T>;
|
|
231
|
+
declare const Schema: Schemastery.Static;
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/types.d.ts
|
|
234
|
+
interface DynamicFilterConfig {
|
|
235
|
+
enable?: boolean;
|
|
236
|
+
notify?: boolean;
|
|
237
|
+
regex?: string;
|
|
238
|
+
keywords?: string[];
|
|
239
|
+
forward?: boolean;
|
|
240
|
+
article?: boolean;
|
|
241
|
+
whitelistEnable?: boolean;
|
|
242
|
+
whitelistRegex?: string;
|
|
243
|
+
whitelistKeywords?: string[];
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/config.d.ts
|
|
247
|
+
interface BilibiliNotifyDynamicConfig {
|
|
248
|
+
logLevel: number;
|
|
249
|
+
dynamicUrl: boolean;
|
|
250
|
+
dynamicCron: string;
|
|
251
|
+
dynamicVideoUrlToBV: boolean;
|
|
252
|
+
pushImgsInDynamic: boolean;
|
|
253
|
+
filter: DynamicFilterConfig & {
|
|
254
|
+
notify?: boolean;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/dynamic-service.d.ts
|
|
259
|
+
declare module "koishi" {
|
|
260
|
+
interface Context {
|
|
261
|
+
"bilibili-notify-dynamic": BilibiliNotifyDynamic;
|
|
262
|
+
}
|
|
263
|
+
interface Events {
|
|
264
|
+
"bilibili-notify/subscription-changed"(subs: Subscriptions): void;
|
|
265
|
+
"bilibili-notify/plugin-error"(source: string, message: string): void;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
declare class BilibiliNotifyDynamic extends Service<BilibiliNotifyDynamicConfig> {
|
|
269
|
+
static readonly [Service.provide] = "bilibili-notify-dynamic";
|
|
270
|
+
private readonly dynamicLogger;
|
|
271
|
+
private api;
|
|
272
|
+
private push;
|
|
273
|
+
private dynamicJob?;
|
|
274
|
+
private dynamicSubManager;
|
|
275
|
+
private dynamicTimelineManager;
|
|
276
|
+
constructor(ctx: Context, config: BilibiliNotifyDynamicConfig);
|
|
277
|
+
protected start(): Awaitable<void>;
|
|
278
|
+
protected stop(): Awaitable<void>;
|
|
279
|
+
get isActive(): boolean;
|
|
280
|
+
startDynamicDetector(subs: Subscriptions): void;
|
|
281
|
+
private detectDynamics;
|
|
282
|
+
private handleApiError;
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/index.d.ts
|
|
286
|
+
declare const name = "bilibili-notify-dynamic";
|
|
287
|
+
declare const inject: {
|
|
288
|
+
required: string[];
|
|
289
|
+
optional: string[];
|
|
290
|
+
};
|
|
291
|
+
type Config = BilibiliNotifyDynamicConfig;
|
|
292
|
+
declare const Config: index_d_exports<BilibiliNotifyDynamicConfig>;
|
|
293
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
294
|
+
//#endregion
|
|
295
|
+
export { BilibiliNotifyDynamic, Config, type DynamicFilterConfig, apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-bilibili-notify-dynamic",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Bilibili 动态推送插件(bilibili-notify 可选插件)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.cjs",
|
|
7
|
+
"types": "./lib/index.d.cts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.cjs",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chatbot",
|
|
15
|
+
"koishi",
|
|
16
|
+
"plugin",
|
|
17
|
+
"bilibili",
|
|
18
|
+
"bilibili-notify"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@bilibili-notify/api": "workspace:^",
|
|
26
|
+
"@bilibili-notify/internal": "workspace:^",
|
|
27
|
+
"@bilibili-notify/push": "workspace:^",
|
|
28
|
+
"cron": "^3.1.7",
|
|
29
|
+
"luxon": "^3.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/luxon": "^3.4.2",
|
|
33
|
+
"koishi-plugin-bilibili-notify": "workspace:^"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"koishi": "^4.18.11",
|
|
37
|
+
"koishi-plugin-bilibili-notify": "workspace:^"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20.0.0"
|
|
41
|
+
},
|
|
42
|
+
"koishi": {
|
|
43
|
+
"service": {
|
|
44
|
+
"required": [
|
|
45
|
+
"bilibili-notify"
|
|
46
|
+
],
|
|
47
|
+
"optional": [
|
|
48
|
+
"bilibili-notify-image"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"description": {
|
|
52
|
+
"zh": "Bilibili 动态推送插件"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"inlinedDependencies": {
|
|
56
|
+
"@standard-schema/spec": "1.1.0",
|
|
57
|
+
"cosmokit": "1.8.1",
|
|
58
|
+
"schemastery": "3.18.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Schema } from "koishi";
|
|
2
|
+
import type { DynamicFilterConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface BilibiliNotifyDynamicConfig {
|
|
5
|
+
logLevel: number;
|
|
6
|
+
dynamicUrl: boolean;
|
|
7
|
+
dynamicCron: string;
|
|
8
|
+
dynamicVideoUrlToBV: boolean;
|
|
9
|
+
pushImgsInDynamic: boolean;
|
|
10
|
+
filter: DynamicFilterConfig & { notify?: boolean };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const BilibiliNotifyDynamicSchema: Schema<BilibiliNotifyDynamicConfig> = Schema.object({
|
|
14
|
+
logLevel: Schema.number()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(3)
|
|
17
|
+
.step(1)
|
|
18
|
+
.default(1)
|
|
19
|
+
.description(
|
|
20
|
+
"这里可以设置日志等级喔~3 是最详细的调试信息,1 是只显示错误信息。主人可以根据需要选择合适的等级,让女仆更好地为您服务 (๑•̀ㅂ•́)و✧",
|
|
21
|
+
),
|
|
22
|
+
dynamicUrl: Schema.boolean()
|
|
23
|
+
.default(false)
|
|
24
|
+
.description(
|
|
25
|
+
"发送动态时要不要顺便发链接呢?但如果主人用的是 QQ 官方机器人,这个开关不要开喔~不然会出事的 (;>_<)!",
|
|
26
|
+
),
|
|
27
|
+
dynamicCron: Schema.string()
|
|
28
|
+
.default("*/2 * * * *")
|
|
29
|
+
.description(
|
|
30
|
+
"主人想多久检查一次动态呢?这里填写 cron 表达式~太短太频繁会吓到女仆的,请温柔一点 (〃ノωノ)",
|
|
31
|
+
),
|
|
32
|
+
dynamicVideoUrlToBV: Schema.boolean()
|
|
33
|
+
.default(false)
|
|
34
|
+
.description("如果是视频动态,开启后会把链接换成 BV 号哦~方便主人的其他用途 (*´・ω・`)"),
|
|
35
|
+
pushImgsInDynamic: Schema.boolean()
|
|
36
|
+
.default(false)
|
|
37
|
+
.description(
|
|
38
|
+
"要不要把动态里的图片也一起推送呢?但、但是可能会触发 QQ 的风控,女仆会有点害怕 (;>_<) 请主人小心决定…",
|
|
39
|
+
),
|
|
40
|
+
filter: Schema.intersect([
|
|
41
|
+
Schema.object({
|
|
42
|
+
enable: Schema.boolean().default(false).description("要开启吗?"),
|
|
43
|
+
}).description("这里是动态屏蔽设置~如果有不想看到的内容,女仆可以帮主人过滤掉 (>﹏<)!"),
|
|
44
|
+
Schema.union([
|
|
45
|
+
Schema.object({
|
|
46
|
+
enable: Schema.const(true).required(),
|
|
47
|
+
notify: Schema.boolean()
|
|
48
|
+
.default(false)
|
|
49
|
+
.description("当动态被屏蔽时,要不要让女仆通知主人呢?"),
|
|
50
|
+
regex: Schema.string().description(
|
|
51
|
+
"这里可以填写正则表达式,用来屏蔽特定动态~女仆会努力匹配的!",
|
|
52
|
+
),
|
|
53
|
+
keywords: Schema.array(String).description(
|
|
54
|
+
"这里填写关键字,每一个都是单独的一项~有这些词的动态女仆都会贴心地拦下来 (*´∀`)",
|
|
55
|
+
),
|
|
56
|
+
forward: Schema.boolean().default(false).description("要不要屏蔽转发动态呢?主人说了算!"),
|
|
57
|
+
article: Schema.boolean()
|
|
58
|
+
.default(false)
|
|
59
|
+
.description("是否屏蔽专栏动态~女仆会按照主人的喜好来处理 (๑•̀ㅂ•́)و✧"),
|
|
60
|
+
whitelistEnable: Schema.boolean()
|
|
61
|
+
.default(false)
|
|
62
|
+
.description("是否启用白名单过滤(仅推送匹配白名单规则的动态)"),
|
|
63
|
+
whitelistRegex: Schema.string().description("白名单正则表达式,命中时允许推送该动态"),
|
|
64
|
+
whitelistKeywords: Schema.array(String).description(
|
|
65
|
+
"白名单关键词,命中任意关键词时允许推送该动态",
|
|
66
|
+
),
|
|
67
|
+
}),
|
|
68
|
+
Schema.object({}),
|
|
69
|
+
]),
|
|
70
|
+
]),
|
|
71
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Dynamic, DynamicFilterConfig, DynamicFilterResult } from "./types";
|
|
2
|
+
import { DynamicFilterReason as Reason } from "./types";
|
|
3
|
+
|
|
4
|
+
function collectRichText(dynamic: Dynamic, texts: string[]): void {
|
|
5
|
+
const richTextNodes = dynamic.modules?.module_dynamic?.desc?.rich_text_nodes;
|
|
6
|
+
if (richTextNodes?.length) {
|
|
7
|
+
texts.push(richTextNodes.map((n) => n.text ?? "").join(""));
|
|
8
|
+
}
|
|
9
|
+
const summaryNodes = dynamic.modules?.module_dynamic?.major?.opus?.summary?.rich_text_nodes;
|
|
10
|
+
if (summaryNodes?.length) {
|
|
11
|
+
texts.push(summaryNodes.map((n) => n.text ?? "").join(""));
|
|
12
|
+
}
|
|
13
|
+
const title = dynamic.modules?.module_dynamic?.major?.opus?.title;
|
|
14
|
+
if (title) texts.push(title);
|
|
15
|
+
const archiveTitle = dynamic.modules?.module_dynamic?.major?.archive?.title;
|
|
16
|
+
if (archiveTitle) texts.push(archiveTitle);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getDynamicText(dynamic: Dynamic): string {
|
|
20
|
+
const texts: string[] = [];
|
|
21
|
+
collectRichText(dynamic, texts);
|
|
22
|
+
if (dynamic.orig) collectRichText(dynamic.orig, texts);
|
|
23
|
+
return texts.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeRegexTest(pattern: string | undefined, text: string): boolean {
|
|
27
|
+
if (!pattern) return false;
|
|
28
|
+
try {
|
|
29
|
+
return new RegExp(pattern).test(text);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[bilibili-notify-dynamic] 无效的正则表达式 "${pattern}": ${(e as Error).message}`,
|
|
33
|
+
);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function testKeywordMatched(text: string, keywords: string[] | undefined): boolean {
|
|
39
|
+
if (!keywords?.length) return false;
|
|
40
|
+
return keywords.some((kw) => kw && text.includes(kw));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function filterDynamic(dynamic: Dynamic, config: DynamicFilterConfig): DynamicFilterResult {
|
|
44
|
+
const cfg = {
|
|
45
|
+
enable: false,
|
|
46
|
+
regex: "",
|
|
47
|
+
keywords: [] as string[],
|
|
48
|
+
forward: false,
|
|
49
|
+
article: false,
|
|
50
|
+
whitelistEnable: false,
|
|
51
|
+
whitelistRegex: "",
|
|
52
|
+
whitelistKeywords: [] as string[],
|
|
53
|
+
...config,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const text = getDynamicText(dynamic);
|
|
57
|
+
|
|
58
|
+
if (cfg.enable) {
|
|
59
|
+
if (cfg.forward && dynamic.type === "DYNAMIC_TYPE_FORWARD") {
|
|
60
|
+
return { blocked: true, reason: Reason.BlacklistForward };
|
|
61
|
+
}
|
|
62
|
+
if (cfg.article && dynamic.type === "DYNAMIC_TYPE_ARTICLE") {
|
|
63
|
+
return { blocked: true, reason: Reason.BlacklistArticle };
|
|
64
|
+
}
|
|
65
|
+
if (safeRegexTest(cfg.regex, text) || testKeywordMatched(text, cfg.keywords)) {
|
|
66
|
+
return { blocked: true, reason: Reason.BlacklistKeyword };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cfg.whitelistEnable) {
|
|
71
|
+
const hasRule = !!cfg.whitelistRegex || cfg.whitelistKeywords.length > 0;
|
|
72
|
+
if (
|
|
73
|
+
hasRule &&
|
|
74
|
+
!safeRegexTest(cfg.whitelistRegex, text) &&
|
|
75
|
+
!testKeywordMatched(text, cfg.whitelistKeywords)
|
|
76
|
+
) {
|
|
77
|
+
return { blocked: true, reason: Reason.WhitelistUnmatched };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { blocked: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type { DynamicFilterConfig, DynamicFilterResult };
|
|
85
|
+
export { Reason as DynamicFilterReason };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { BilibiliAPI } from "@bilibili-notify/api";
|
|
2
|
+
import { BILIBILI_NOTIFY_TOKEN } from "@bilibili-notify/internal";
|
|
3
|
+
import type { BilibiliPush, SubManager, Subscriptions } from "@bilibili-notify/push";
|
|
4
|
+
import { PushType } from "@bilibili-notify/push";
|
|
5
|
+
import { CronJob } from "cron";
|
|
6
|
+
import { type Awaitable, type Context, h, Logger, Service } from "koishi";
|
|
7
|
+
// biome-ignore lint/correctness/noUnusedImports: <empty import> is needed to make sure the type augmentation works
|
|
8
|
+
import {} from "koishi-plugin-bilibili-notify";
|
|
9
|
+
import { DateTime } from "luxon";
|
|
10
|
+
import type { BilibiliNotifyDynamicConfig } from "./config";
|
|
11
|
+
import { DynamicFilterReason, filterDynamic } from "./dynamic-filter";
|
|
12
|
+
import type { AllDynamicInfo, Dynamic, DynamicTimelineManager } from "./types";
|
|
13
|
+
|
|
14
|
+
declare module "koishi" {
|
|
15
|
+
interface Context {
|
|
16
|
+
"bilibili-notify-dynamic": BilibiliNotifyDynamic;
|
|
17
|
+
}
|
|
18
|
+
interface Events {
|
|
19
|
+
"bilibili-notify/subscription-changed"(subs: Subscriptions): void;
|
|
20
|
+
"bilibili-notify/plugin-error"(source: string, message: string): void;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SERVICE_NAME = "bilibili-notify-dynamic";
|
|
25
|
+
|
|
26
|
+
/** Simple async lock: if the previous run is still executing, skip. */
|
|
27
|
+
function withLock(fn: () => Promise<void>): () => void {
|
|
28
|
+
let locked = false;
|
|
29
|
+
return () => {
|
|
30
|
+
if (locked) return;
|
|
31
|
+
locked = true;
|
|
32
|
+
fn()
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
console.error("[bilibili-notify-dynamic] Execution error:", err);
|
|
35
|
+
})
|
|
36
|
+
.finally(() => {
|
|
37
|
+
locked = false;
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class BilibiliNotifyDynamic extends Service<BilibiliNotifyDynamicConfig> {
|
|
43
|
+
static readonly [Service.provide] = SERVICE_NAME;
|
|
44
|
+
|
|
45
|
+
private readonly dynamicLogger: Logger;
|
|
46
|
+
private api!: BilibiliAPI;
|
|
47
|
+
private push!: BilibiliPush;
|
|
48
|
+
private dynamicJob?: CronJob;
|
|
49
|
+
private dynamicSubManager: SubManager = new Map();
|
|
50
|
+
private dynamicTimelineManager: DynamicTimelineManager = new Map();
|
|
51
|
+
|
|
52
|
+
constructor(ctx: Context, config: BilibiliNotifyDynamicConfig) {
|
|
53
|
+
super(ctx, SERVICE_NAME);
|
|
54
|
+
this.config = config;
|
|
55
|
+
this.dynamicLogger = new Logger(SERVICE_NAME);
|
|
56
|
+
this.dynamicLogger.level = config.logLevel;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected start(): Awaitable<void> {
|
|
60
|
+
const internals = this.ctx["bilibili-notify"].getInternals(BILIBILI_NOTIFY_TOKEN);
|
|
61
|
+
if (!internals) throw new Error("无法获取 bilibili-notify 内部实例,请确认核心插件已启动");
|
|
62
|
+
this.api = internals.api;
|
|
63
|
+
this.push = internals.push;
|
|
64
|
+
this.dynamicTimelineManager = new Map();
|
|
65
|
+
// If subscriptions were already loaded before this plugin started, start immediately
|
|
66
|
+
if (internals.subs) {
|
|
67
|
+
this.startDynamicDetector(internals.subs);
|
|
68
|
+
}
|
|
69
|
+
// Listen for future subscription changes from core
|
|
70
|
+
this.ctx.on("bilibili-notify/subscription-changed", (subs: Subscriptions) => {
|
|
71
|
+
this.startDynamicDetector(subs);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected stop(): Awaitable<void> {
|
|
76
|
+
if (this.dynamicJob) {
|
|
77
|
+
this.dynamicJob.stop();
|
|
78
|
+
this.dynamicLogger.info("动态检测任务已停止");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get isActive(): boolean {
|
|
83
|
+
return this.dynamicJob?.running ?? false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
startDynamicDetector(subs: Subscriptions): void {
|
|
87
|
+
// Stop existing job first
|
|
88
|
+
if (this.dynamicJob) {
|
|
89
|
+
this.dynamicJob.stop();
|
|
90
|
+
this.dynamicJob = undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build sub manager with only dynamic-enabled subs
|
|
94
|
+
const dynamicSubManager: SubManager = new Map();
|
|
95
|
+
for (const sub of Object.values(subs)) {
|
|
96
|
+
if (sub.dynamic) {
|
|
97
|
+
// 只为新增 UID 设置初始时间戳,保留已有 UID 的时间戳避免重推旧动态
|
|
98
|
+
if (!this.dynamicTimelineManager.has(sub.uid)) {
|
|
99
|
+
this.dynamicTimelineManager.set(sub.uid, Math.floor(DateTime.now().toSeconds()));
|
|
100
|
+
}
|
|
101
|
+
dynamicSubManager.set(sub.uid, sub);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 清理已移除 UID 的时间戳记录
|
|
105
|
+
for (const uid of this.dynamicTimelineManager.keys()) {
|
|
106
|
+
if (!dynamicSubManager.has(uid)) {
|
|
107
|
+
this.dynamicTimelineManager.delete(uid);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (dynamicSubManager.size === 0) {
|
|
112
|
+
this.dynamicLogger.info("没有需要动态检测的订阅对象");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.dynamicSubManager = dynamicSubManager;
|
|
117
|
+
|
|
118
|
+
this.dynamicJob = new CronJob(
|
|
119
|
+
this.config.dynamicCron,
|
|
120
|
+
withLock(() => this.detectDynamics()),
|
|
121
|
+
);
|
|
122
|
+
this.dynamicJob.start();
|
|
123
|
+
this.dynamicLogger.info("动态检测任务已启动");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async detectDynamics(): Promise<void> {
|
|
127
|
+
this.dynamicLogger.debug("开始获取动态信息");
|
|
128
|
+
|
|
129
|
+
let content: AllDynamicInfo | undefined;
|
|
130
|
+
try {
|
|
131
|
+
content = (await this.api.getAllDynamic()) as AllDynamicInfo;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
this.dynamicLogger.error(`获取动态失败:${e}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!content) return;
|
|
138
|
+
|
|
139
|
+
if (content.code !== 0) {
|
|
140
|
+
await this.handleApiError(content.code, content.message);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.dynamicLogger.debug("成功获取动态信息,开始处理");
|
|
145
|
+
|
|
146
|
+
const currentPushDyn: Record<string, Dynamic> = {};
|
|
147
|
+
|
|
148
|
+
for (const item of content.data.items) {
|
|
149
|
+
if (!item) continue;
|
|
150
|
+
|
|
151
|
+
const postTime = item.modules.module_author.pub_ts;
|
|
152
|
+
if (typeof postTime !== "number" || !Number.isFinite(postTime)) {
|
|
153
|
+
this.dynamicLogger.warn(
|
|
154
|
+
`跳过无效动态:pub_ts 缺失或非数字,ID=${item.id_str ?? "unknown"}`,
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const uid = item.modules.module_author.mid.toString();
|
|
160
|
+
const name = item.modules.module_author.name;
|
|
161
|
+
|
|
162
|
+
const timeline = this.dynamicTimelineManager.get(uid);
|
|
163
|
+
if (timeline === undefined) continue; // not subscribed
|
|
164
|
+
|
|
165
|
+
this.dynamicLogger.debug(
|
|
166
|
+
`检查动态 UP=${name} UID=${uid} 发布时间=${DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (timeline >= postTime) continue; // already pushed
|
|
170
|
+
|
|
171
|
+
// Filter
|
|
172
|
+
const filterResult = filterDynamic(item, this.config.filter ?? {});
|
|
173
|
+
if (filterResult.blocked) {
|
|
174
|
+
if (this.config.filter?.notify) {
|
|
175
|
+
const msgs: Record<DynamicFilterReason, string> = {
|
|
176
|
+
[DynamicFilterReason.BlacklistKeyword]: `${name}发布了一条含有屏蔽关键字的动态`,
|
|
177
|
+
[DynamicFilterReason.BlacklistForward]: `${name}转发了一条动态,已屏蔽`,
|
|
178
|
+
[DynamicFilterReason.BlacklistArticle]: `${name}投稿了一条专栏,已屏蔽`,
|
|
179
|
+
[DynamicFilterReason.WhitelistUnmatched]: `${name}发布了一条不在白名单范围内的动态,已屏蔽`,
|
|
180
|
+
};
|
|
181
|
+
await this.push.broadcastToTargets(
|
|
182
|
+
uid,
|
|
183
|
+
h("message", msgs[filterResult.reason as DynamicFilterReason]),
|
|
184
|
+
PushType.Dynamic,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Render card
|
|
191
|
+
const sub = this.dynamicSubManager.get(uid);
|
|
192
|
+
// biome-ignore lint/suspicious/noExplicitAny: optional image service
|
|
193
|
+
const imageService = (this.ctx as any)["bilibili-notify-image"];
|
|
194
|
+
// biome-ignore lint/suspicious/noExplicitAny: image buffer
|
|
195
|
+
let buffer: any;
|
|
196
|
+
try {
|
|
197
|
+
if (imageService?.generateDynamicCard) {
|
|
198
|
+
buffer = await imageService.generateDynamicCard(
|
|
199
|
+
item,
|
|
200
|
+
sub?.customCardStyle?.enable ? sub.customCardStyle : undefined,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
const err = e as Error;
|
|
205
|
+
if (err.message === "直播开播动态,不做处理") continue;
|
|
206
|
+
this.dynamicLogger.error(`生成动态图片失败:${err.message},动态检测已停止`);
|
|
207
|
+
await this.push.sendErrorMsg(`生成动态图片失败:${err.message},动态检测已停止`);
|
|
208
|
+
this.dynamicJob?.stop();
|
|
209
|
+
this.dynamicJob = undefined;
|
|
210
|
+
this.ctx.emit(
|
|
211
|
+
"bilibili-notify/plugin-error",
|
|
212
|
+
SERVICE_NAME,
|
|
213
|
+
`生成动态图片失败:${err.message}`,
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Build URL suffix
|
|
219
|
+
let dUrl = "";
|
|
220
|
+
if (this.config.dynamicUrl) {
|
|
221
|
+
if (item.type === "DYNAMIC_TYPE_AV") {
|
|
222
|
+
const jumpUrl = item.modules.module_dynamic.major?.archive?.jump_url ?? "";
|
|
223
|
+
if (this.config.dynamicVideoUrlToBV) {
|
|
224
|
+
const bvMatch = jumpUrl.match(/BV[0-9A-Za-z]+/);
|
|
225
|
+
dUrl = bvMatch ? bvMatch[0] : "";
|
|
226
|
+
} else {
|
|
227
|
+
dUrl = `${name}发布了新视频:https:${jumpUrl}`;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
dUrl = `${name}发布了一条动态:https://t.bilibili.com/${item.id_str}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Send
|
|
235
|
+
const msgContent = buffer
|
|
236
|
+
? [h.image(buffer, "image/jpeg"), h.text(dUrl)]
|
|
237
|
+
: [h.text(`${name}发布了一条动态${dUrl ? `:${dUrl}` : ""}`)];
|
|
238
|
+
await this.push.broadcastToTargets(uid, h("message", msgContent), PushType.Dynamic);
|
|
239
|
+
|
|
240
|
+
// Push extra images from draw dynamics
|
|
241
|
+
if (this.config.pushImgsInDynamic && item.type === "DYNAMIC_TYPE_DRAW") {
|
|
242
|
+
const pics = item.modules?.module_dynamic?.major?.opus?.pics;
|
|
243
|
+
if (pics?.length) {
|
|
244
|
+
const picsMsg = h(
|
|
245
|
+
"message",
|
|
246
|
+
{ forward: true },
|
|
247
|
+
pics.map((p) => h.img(p.url)),
|
|
248
|
+
);
|
|
249
|
+
await this.push.broadcastToTargets(uid, picsMsg, PushType.Dynamic);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Track the earliest new dynamic per UID
|
|
254
|
+
if (!currentPushDyn[uid]) {
|
|
255
|
+
currentPushDyn[uid] = item;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Update timelines
|
|
260
|
+
for (const [uid, item] of Object.entries(currentPushDyn)) {
|
|
261
|
+
const postTime = item.modules.module_author.pub_ts;
|
|
262
|
+
this.dynamicTimelineManager.set(uid, postTime);
|
|
263
|
+
this.dynamicLogger.debug(
|
|
264
|
+
`更新时间线 UID=${uid} 时间=${DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.dynamicLogger.debug(`本次推送 ${Object.keys(currentPushDyn).length} 条动态`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async handleApiError(code: number, message: string): Promise<void> {
|
|
272
|
+
// Stop dynamic detector first
|
|
273
|
+
this.dynamicJob?.stop();
|
|
274
|
+
this.dynamicJob = undefined;
|
|
275
|
+
switch (code) {
|
|
276
|
+
case -101: {
|
|
277
|
+
this.dynamicLogger.error("账号未登录,动态检测已停止");
|
|
278
|
+
await this.push.sendPrivateMsg("账号未登录,请先登录");
|
|
279
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, "账号未登录");
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case -352: {
|
|
283
|
+
this.dynamicLogger.error("账号被风控,动态检测已停止");
|
|
284
|
+
await this.push.sendPrivateMsg("账号被风控,请使用 `bili cap` 指令解除风控");
|
|
285
|
+
this.ctx.emit("bilibili-notify/plugin-error", SERVICE_NAME, "账号被风控");
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
default: {
|
|
289
|
+
this.dynamicLogger.error(`获取动态信息失败,错误码:${code},${message}`);
|
|
290
|
+
await this.push.sendPrivateMsg(`获取动态信息失败,错误码:${code}`);
|
|
291
|
+
this.ctx.emit(
|
|
292
|
+
"bilibili-notify/plugin-error",
|
|
293
|
+
SERVICE_NAME,
|
|
294
|
+
`获取动态失败,错误码:${code}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from "koishi";
|
|
2
|
+
import { type BilibiliNotifyDynamicConfig, BilibiliNotifyDynamicSchema } from "./config";
|
|
3
|
+
import { BilibiliNotifyDynamic } from "./dynamic-service";
|
|
4
|
+
import type { DynamicFilterConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
export type { DynamicFilterConfig };
|
|
7
|
+
export { BilibiliNotifyDynamic };
|
|
8
|
+
|
|
9
|
+
export const name = "bilibili-notify-dynamic";
|
|
10
|
+
|
|
11
|
+
export const inject = {
|
|
12
|
+
required: ["bilibili-notify"],
|
|
13
|
+
optional: ["bilibili-notify-image"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type Config = BilibiliNotifyDynamicConfig;
|
|
17
|
+
export const Config = BilibiliNotifyDynamicSchema;
|
|
18
|
+
|
|
19
|
+
export function apply(ctx: Context, config: Config): void {
|
|
20
|
+
ctx.plugin(BilibiliNotifyDynamic, config);
|
|
21
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ---- Dynamic API response types ----
|
|
2
|
+
|
|
3
|
+
export type RichTextNode = {
|
|
4
|
+
orig_text: string;
|
|
5
|
+
text: string;
|
|
6
|
+
type: string;
|
|
7
|
+
emoji?: { icon_url: string; size: number; text: string; type: number };
|
|
8
|
+
jump_url?: string;
|
|
9
|
+
rid?: string;
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: API response
|
|
11
|
+
goods?: any;
|
|
12
|
+
icon_name?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Dynamic = {
|
|
16
|
+
// biome-ignore lint/complexity/noBannedTypes: API response shape
|
|
17
|
+
basic: Object;
|
|
18
|
+
id_str: string;
|
|
19
|
+
type: string;
|
|
20
|
+
orig?: Dynamic;
|
|
21
|
+
modules: {
|
|
22
|
+
module_author: {
|
|
23
|
+
face: string;
|
|
24
|
+
following: boolean;
|
|
25
|
+
jump_url: string;
|
|
26
|
+
label: string;
|
|
27
|
+
mid: number;
|
|
28
|
+
name: string;
|
|
29
|
+
pub_action: string;
|
|
30
|
+
pub_time: string;
|
|
31
|
+
pub_ts: number;
|
|
32
|
+
type: string;
|
|
33
|
+
// biome-ignore lint/suspicious/noExplicitAny: API response
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
};
|
|
36
|
+
module_dynamic: {
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: API response
|
|
38
|
+
additional?: any;
|
|
39
|
+
desc?: {
|
|
40
|
+
rich_text_nodes: RichTextNode[];
|
|
41
|
+
text: string;
|
|
42
|
+
};
|
|
43
|
+
major?: {
|
|
44
|
+
opus?: {
|
|
45
|
+
fold_action: string[];
|
|
46
|
+
jump_url: string;
|
|
47
|
+
pics: Array<{
|
|
48
|
+
height: number;
|
|
49
|
+
live_url: string;
|
|
50
|
+
size: number;
|
|
51
|
+
url: string;
|
|
52
|
+
width: number;
|
|
53
|
+
}>;
|
|
54
|
+
summary: {
|
|
55
|
+
rich_text_nodes: RichTextNode[];
|
|
56
|
+
text: string;
|
|
57
|
+
};
|
|
58
|
+
title: string;
|
|
59
|
+
};
|
|
60
|
+
archive?: {
|
|
61
|
+
title: string;
|
|
62
|
+
jump_url: string;
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: API response
|
|
64
|
+
[key: string]: any;
|
|
65
|
+
};
|
|
66
|
+
// biome-ignore lint/suspicious/noExplicitAny: API response
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type AllDynamicInfo = {
|
|
74
|
+
code: number;
|
|
75
|
+
message: string;
|
|
76
|
+
data: {
|
|
77
|
+
has_more: boolean;
|
|
78
|
+
items: Dynamic[];
|
|
79
|
+
offset: string;
|
|
80
|
+
update_baseline: string;
|
|
81
|
+
update_num: number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type DynamicTimelineManager = Map<string, number>;
|
|
86
|
+
|
|
87
|
+
// ---- Filter types ----
|
|
88
|
+
|
|
89
|
+
export interface DynamicFilterConfig {
|
|
90
|
+
enable?: boolean;
|
|
91
|
+
notify?: boolean;
|
|
92
|
+
regex?: string;
|
|
93
|
+
keywords?: string[];
|
|
94
|
+
forward?: boolean;
|
|
95
|
+
article?: boolean;
|
|
96
|
+
whitelistEnable?: boolean;
|
|
97
|
+
whitelistRegex?: string;
|
|
98
|
+
whitelistKeywords?: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export enum DynamicFilterReason {
|
|
102
|
+
BlacklistKeyword = "blacklist-keyword",
|
|
103
|
+
BlacklistForward = "blacklist-forward",
|
|
104
|
+
BlacklistArticle = "blacklist-article",
|
|
105
|
+
WhitelistUnmatched = "whitelist-unmatched",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DynamicFilterResult {
|
|
109
|
+
blocked: boolean;
|
|
110
|
+
reason?: DynamicFilterReason;
|
|
111
|
+
}
|
package/tsconfig.json
ADDED