koishi-plugin-18xx 0.0.7 → 0.0.9
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/lib/config.d.ts +2 -0
- package/lib/index.js +112 -41
- package/package.json +3 -2
package/lib/config.d.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Server } from '@koishijs/plugin-server';
|
2
2
|
import { Schema } from 'koishi';
|
3
|
+
export type SendMode = 'private-only' | 'private-guild' | 'guild-only';
|
3
4
|
export declare const name = "18xx";
|
4
5
|
export declare const inject: {
|
5
6
|
required: string[];
|
@@ -17,6 +18,7 @@ export interface Config {
|
|
17
18
|
guildIds: string[];
|
18
19
|
defaultBotIds: string[];
|
19
20
|
botIds: string[];
|
21
|
+
sendMode: 'private-only' | 'private-guild' | 'guild-only';
|
20
22
|
}[];
|
21
23
|
};
|
22
24
|
}
|
package/lib/index.js
CHANGED
@@ -69,7 +69,12 @@ var Config = import_koishi.Schema.object({
|
|
69
69
|
defaultGuildId: import_koishi.Schema.string().default("").description("默认发送通知的群,如果不指定,通知会被发送到绑定时的群,如果绑定时没有群,通知不会发送"),
|
70
70
|
guildIds: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("允许发送通知的群,为空表示允许在所有群发送通知"),
|
71
71
|
defaultBotIds: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("默认发送通知的机器人,如果不指定,通知由绑定时的机器人发送"),
|
72
|
-
botIds: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("允许发送通知的机器人,如果先前的机器人都无法工作,将会尝试使用后面的机器人发送通知,为空表示允许所有机器人发送通知")
|
72
|
+
botIds: import_koishi.Schema.array(import_koishi.Schema.string()).default([]).description("允许发送通知的机器人,如果先前的机器人都无法工作,将会尝试使用后面的机器人发送通知,为空表示允许所有机器人发送通知"),
|
73
|
+
sendMode: import_koishi.Schema.union([
|
74
|
+
import_koishi.Schema.const("private-only").description("仅私聊"),
|
75
|
+
import_koishi.Schema.const("private-guild").description("私聊优先,失败则群聊"),
|
76
|
+
import_koishi.Schema.const("guild-only").description("仅群聊")
|
77
|
+
]).default("private-guild").description("通知发送方式")
|
73
78
|
})
|
74
79
|
).default([]).description("平台通知设置")
|
75
80
|
}).description("通知设置")
|
@@ -99,7 +104,7 @@ function command(ctx, config) {
|
|
99
104
|
if (!found) return "";
|
100
105
|
}
|
101
106
|
}, "checkSessionMiddleware");
|
102
|
-
ctx.command("18xx.bind <id>", "绑定账号").option("force", "-f", { authority: 4 }).usage("id 是个人资料页地址栏 profile 后面的数字").before(checkSessionMiddleware).action(async ({ session, options }, id) => {
|
107
|
+
ctx.command("18xx.bind <id>", "绑定账号").option("force", "-f", { authority: 4 }).usage("id 是个人资料页地址栏 profile 后面的数字").usage("需要先绑定账号,并在个人资料页 Webhook User ID 中填入你的 id").before(checkSessionMiddleware).action(async ({ session, options }, id) => {
|
103
108
|
if (Number(id)) {
|
104
109
|
if (!options.force) {
|
105
110
|
const profiles = await ctx.database.get(name, { id: Number(id) });
|
@@ -140,7 +145,7 @@ function command(ctx, config) {
|
|
140
145
|
}
|
141
146
|
return "解绑失败";
|
142
147
|
});
|
143
|
-
ctx.command("18xx.list", "列出已绑定的账号").alias("18xx.ls").action(async ({ session }) => {
|
148
|
+
ctx.command("18xx.list", "列出已绑定的账号").usage("需要先绑定账号,并在个人资料页 Webhook User ID 中填入你的 id").alias("18xx.ls").action(async ({ session }) => {
|
144
149
|
const profiles = await ctx.database.get(name, { userId: session.userId });
|
145
150
|
if (!profiles.length) {
|
146
151
|
return "你还没有绑定账号";
|
@@ -148,18 +153,15 @@ function command(ctx, config) {
|
|
148
153
|
return `当前已绑定${profiles.length}个账号:
|
149
154
|
${profiles.map((p) => `${p.id}`).join("\n")}`;
|
150
155
|
});
|
151
|
-
ctx.command("18xx.on", "开启通知").usage("需要先绑定账号,并在个人资料页
|
156
|
+
ctx.command("18xx.on", "开启通知").usage("需要先绑定账号,并在个人资料页 Webhook User ID 中填入你的 id").action(async ({ session }) => {
|
152
157
|
const profiles = await ctx.database.get(name, { userId: session.userId });
|
153
158
|
if (!profiles.length) {
|
154
159
|
return "你还没有绑定账号";
|
155
160
|
}
|
156
|
-
|
161
|
+
await ctx.database.upsert(
|
157
162
|
name,
|
158
163
|
profiles.map((p) => ({ ...p, notify: true }))
|
159
164
|
);
|
160
|
-
if (result.matched > 1) {
|
161
|
-
return `开启了${result.matched || 0}个账号的通知`;
|
162
|
-
}
|
163
165
|
return "通知已开启";
|
164
166
|
});
|
165
167
|
ctx.command("18xx.off", "关闭通知").action(async ({ session }) => {
|
@@ -167,20 +169,48 @@ ${profiles.map((p) => `${p.id}`).join("\n")}`;
|
|
167
169
|
if (!profiles.length) {
|
168
170
|
return "你还没有绑定账号";
|
169
171
|
}
|
170
|
-
|
172
|
+
await ctx.database.upsert(
|
171
173
|
name,
|
172
174
|
profiles.map((p) => ({ ...p, notify: false }))
|
173
175
|
);
|
174
|
-
if (result.matched > 1) {
|
175
|
-
return `关闭了${result.matched || 0}个账号的通知`;
|
176
|
-
}
|
177
176
|
return "通知已关闭";
|
178
177
|
});
|
178
|
+
ctx.command("18xx.config <key> <value>", "修改设置").usage("需要先绑定账号,并在个人资料页 Webhook User ID 中填入你的 id").action(async ({ session }, key, value) => {
|
179
|
+
const profiles = await ctx.database.get(name, { userId: session.userId });
|
180
|
+
if (!profiles.length) {
|
181
|
+
return "你还没有绑定账号";
|
182
|
+
}
|
183
|
+
if (typeof key !== "string" || typeof value !== "string") {
|
184
|
+
return "参数错误";
|
185
|
+
}
|
186
|
+
let val = value;
|
187
|
+
switch (key) {
|
188
|
+
case "notify":
|
189
|
+
val = value !== "0" && Boolean(value);
|
190
|
+
break;
|
191
|
+
case "interval":
|
192
|
+
val = Math.max(10, Math.min(600, Number(value)));
|
193
|
+
break;
|
194
|
+
}
|
195
|
+
try {
|
196
|
+
const result = await ctx.database.upsert(
|
197
|
+
name,
|
198
|
+
profiles.map((p) => ({ ...p, [key]: val }))
|
199
|
+
);
|
200
|
+
if (result.matched) {
|
201
|
+
return `设置已修改:${key}=${val}`;
|
202
|
+
}
|
203
|
+
} catch (e) {
|
204
|
+
logger.error("Cannot update config:", e?.message || e);
|
205
|
+
return `未能修改设置:${key}=${val}`;
|
206
|
+
}
|
207
|
+
});
|
179
208
|
}
|
180
209
|
__name(command, "command");
|
181
210
|
|
182
211
|
// src/server.tsx
|
183
212
|
var import_plugin_server2 = __toESM(require("@koishijs/plugin-server"));
|
213
|
+
var import_lru_cache = require("lru-cache");
|
184
214
|
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
185
215
|
var ForkServer = class extends import_plugin_server2.default {
|
186
216
|
static {
|
@@ -205,7 +235,9 @@ var forkServer = /* @__PURE__ */ __name((ctx, config) => new Promise((resolve, r
|
|
205
235
|
proxy.once("server/ready", () => resolve(proxy.server));
|
206
236
|
proxy.once("dispose", reject);
|
207
237
|
}), "forkServer");
|
208
|
-
var MESSAGE_REGEX = /^<@?(.*?)>\s*(
|
238
|
+
var MESSAGE_REGEX = /^<@?(.*?)>\s*([\S\s]*)$/;
|
239
|
+
var GAME_REGEX = /18xx\.games\/game\/(\d+)/;
|
240
|
+
var sendCache = new import_lru_cache.LRUCache({ max: 1e3 });
|
209
241
|
async function server(ctx, config) {
|
210
242
|
const logger = ctx.logger(name);
|
211
243
|
let server2 = ctx.server;
|
@@ -216,51 +248,83 @@ async function server(ctx, config) {
|
|
216
248
|
if (config.notification.enable) {
|
217
249
|
server2.post(config.notification.path, async (ptx, _next) => {
|
218
250
|
const { text = "" } = ptx.request.body || {};
|
219
|
-
|
220
|
-
|
221
|
-
logger.error("Webhook格式错误", text);
|
251
|
+
if (!text) {
|
252
|
+
logger.error("Webhook 格式错误, body", ptx.request.body);
|
222
253
|
return ptx.status = 400;
|
223
254
|
}
|
224
|
-
const
|
225
|
-
|
226
|
-
|
255
|
+
const matchMessage = MESSAGE_REGEX.exec(text);
|
256
|
+
const webhookId = Number(matchMessage?.[1]);
|
257
|
+
const message = matchMessage?.[2] || "";
|
258
|
+
if (!webhookId || !message) {
|
259
|
+
logger.error("Webhook 格式错误, text", text);
|
227
260
|
return ptx.status = 400;
|
228
261
|
}
|
229
|
-
const
|
230
|
-
|
231
|
-
const
|
232
|
-
|
262
|
+
const matchGame = GAME_REGEX.exec(message);
|
263
|
+
const gameId = matchGame?.[1] || "";
|
264
|
+
const profiles = await ctx.database.get(name, { id: webhookId });
|
265
|
+
logger.debug("Webhook", webhookId, gameId, message, profiles);
|
266
|
+
const sendNotification = /* @__PURE__ */ __name(async (profile, sendMode = "private-guild", bots = ctx.bots, guildIds = [profile.guildId]) => {
|
267
|
+
const sendCacheKey = [webhookId, profile.userId, gameId].join("|");
|
233
268
|
for (const bot of bots) {
|
234
269
|
for (const guildId of guildIds) {
|
235
270
|
try {
|
236
|
-
|
271
|
+
let result;
|
272
|
+
const sendPrivate = /* @__PURE__ */ __name(() => bot.sendPrivateMessage(profile.userId, message, guildId), "sendPrivate");
|
273
|
+
const sendGuild = /* @__PURE__ */ __name(() => bot.sendMessage(
|
237
274
|
guildId,
|
238
275
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
239
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("at", { id: profile.userId, name: webhookId }),
|
276
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("at", { id: profile.userId, name: `${webhookId}` }),
|
240
277
|
" ",
|
241
278
|
message
|
242
279
|
] })
|
243
|
-
);
|
244
|
-
|
245
|
-
|
246
|
-
|
280
|
+
), "sendGuild");
|
281
|
+
switch (sendMode) {
|
282
|
+
case "private-only":
|
283
|
+
result = await sendPrivate();
|
284
|
+
break;
|
285
|
+
case "private-guild":
|
286
|
+
try {
|
287
|
+
result = await sendPrivate();
|
288
|
+
} catch (e) {
|
289
|
+
logger.error("私聊消息发送失败,尝试群消息", e, sendCacheKey, message);
|
290
|
+
result = await sendGuild();
|
291
|
+
}
|
292
|
+
break;
|
293
|
+
case "guild-only":
|
294
|
+
result = await sendGuild();
|
295
|
+
break;
|
296
|
+
}
|
297
|
+
if (result?.length > 0) {
|
298
|
+
logger.info("通知发送成功", result, sendCacheKey, message);
|
299
|
+
sendCache.set(sendCacheKey, { ts: Date.now() });
|
300
|
+
return true;
|
301
|
+
}
|
247
302
|
} catch (e) {
|
248
|
-
logger.error("通知发送失败", e,
|
303
|
+
logger.error("通知发送失败", e, sendCacheKey, message);
|
249
304
|
}
|
250
305
|
}
|
251
|
-
if (success) {
|
252
|
-
break;
|
253
|
-
}
|
254
|
-
}
|
255
|
-
if (!success) {
|
256
|
-
logger.info("未能发送通知", message, profile);
|
257
306
|
}
|
258
|
-
|
307
|
+
logger.info("未能发送通知", sendCacheKey, message);
|
308
|
+
return false;
|
259
309
|
}, "sendNotification");
|
260
|
-
|
310
|
+
const tryProfile = /* @__PURE__ */ __name(async (profile) => {
|
311
|
+
const sendCacheKey = [webhookId, profile.userId, gameId].join("|");
|
312
|
+
if (sendCache.has(sendCacheKey)) {
|
313
|
+
const { ts: lastTimestamp, cancel: cancelPrevTask } = sendCache.get(sendCacheKey);
|
314
|
+
cancelPrevTask?.();
|
315
|
+
const nextTimestamp = lastTimestamp + profile.interval * 1e3;
|
316
|
+
const now = Date.now();
|
317
|
+
if (nextTimestamp > now) {
|
318
|
+
logger.debug("通知发送间隔小于限制", sendCacheKey, nextTimestamp - now);
|
319
|
+
const cancel = ctx.setTimeout(() => tryProfile(profile), nextTimestamp - now);
|
320
|
+
sendCache.set(sendCacheKey, { ts: lastTimestamp, cancel });
|
321
|
+
return;
|
322
|
+
}
|
323
|
+
sendCache.delete(sendCacheKey);
|
324
|
+
}
|
261
325
|
if (!config.notification.items.length) {
|
262
326
|
await sendNotification(profile);
|
263
|
-
|
327
|
+
return;
|
264
328
|
}
|
265
329
|
for (const item of config.notification.items.filter((item2) => !item2.platform || item2.platform === profile.platform)) {
|
266
330
|
if (item.botIds.length && !item.botIds.includes(profile.botId) && !item.defaultBotIds.includes(profile.botId)) {
|
@@ -279,10 +343,13 @@ async function server(ctx, config) {
|
|
279
343
|
)
|
280
344
|
);
|
281
345
|
const guildIds = Array.from(new Set([item.defaultGuildId, profile.guildId, ...item.guildIds].filter(Boolean)));
|
282
|
-
if (await sendNotification(profile, bots, guildIds)) {
|
283
|
-
|
346
|
+
if (await sendNotification(profile, item.sendMode, bots, guildIds)) {
|
347
|
+
return;
|
284
348
|
}
|
285
349
|
}
|
350
|
+
}, "tryProfile");
|
351
|
+
for (const profile of profiles.filter((profile2) => profile2.notify)) {
|
352
|
+
tryProfile(profile);
|
286
353
|
}
|
287
354
|
return ptx.status = 200;
|
288
355
|
});
|
@@ -302,6 +369,10 @@ function apply(ctx, config) {
|
|
302
369
|
notify: {
|
303
370
|
type: "boolean",
|
304
371
|
initial: true
|
372
|
+
},
|
373
|
+
interval: {
|
374
|
+
type: "integer",
|
375
|
+
initial: 30
|
305
376
|
}
|
306
377
|
});
|
307
378
|
command(ctx, config);
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "koishi-plugin-18xx",
|
3
3
|
"description": "18xxGames 机器人",
|
4
|
-
"version": "0.0.
|
4
|
+
"version": "0.0.9",
|
5
5
|
"main": "lib/index.js",
|
6
6
|
"typings": "lib/index.d.ts",
|
7
7
|
"files": [
|
@@ -29,7 +29,8 @@
|
|
29
29
|
"peerDependencies": {
|
30
30
|
"@koishijs/plugin-server": "^3.2.7",
|
31
31
|
"@satorijs/element": "^3.1.8",
|
32
|
-
"koishi": "^4.18.7"
|
32
|
+
"koishi": "^4.18.7",
|
33
|
+
"lru-cache": "^11.1.0"
|
33
34
|
},
|
34
35
|
"koishi": {
|
35
36
|
"description": {
|