koishi-plugin-18xx 0.0.8 → 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 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("需要先绑定账号,并在个人资料页 webhook_user_id 中填入你的 id").action(async ({ session }) => {
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
- const result = await ctx.database.upsert(
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
- const result = await ctx.database.upsert(
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 {
@@ -206,6 +236,8 @@ var forkServer = /* @__PURE__ */ __name((ctx, config) => new Promise((resolve, r
206
236
  proxy.once("dispose", reject);
207
237
  }), "forkServer");
208
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
- const result = MESSAGE_REGEX.exec(text);
220
- if (!result) {
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 [, webhookId, message] = result;
225
- if (!Number(webhookId) || !message) {
226
- logger.error("Webhook格式错误", text);
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 profiles = await ctx.database.get(name, { id: Number(webhookId) });
230
- logger.info("Webhook", webhookId, message, profiles);
231
- const sendNotification = /* @__PURE__ */ __name(async (profile, bots = ctx.bots, guildIds = [profile.guildId]) => {
232
- let success = false;
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
- const result2 = await bot.sendMessage(
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
- logger.info("通知发送成功", result2, message, profile);
245
- success = true;
246
- break;
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, message, profile);
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
- return success;
307
+ logger.info("未能发送通知", sendCacheKey, message);
308
+ return false;
259
309
  }, "sendNotification");
260
- for (const profile of profiles.filter((profile2) => profile2.notify)) {
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
- continue;
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
- break;
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.8",
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": {