koishi-plugin-maple-drift-bottle 0.0.3 → 0.1.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/lib/index.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "maple-drift-bottle";
3
+ export declare const using: readonly ["database"];
4
+ interface DriftBottle {
5
+ id: number;
6
+ author: string;
7
+ authorName: string;
8
+ anonymous: boolean;
9
+ content: string;
10
+ image: string | null;
11
+ created: Date;
12
+ }
13
+ interface BottleComment {
14
+ id: number;
15
+ bottleId: number;
16
+ author: string;
17
+ authorName: string;
18
+ anonymous: boolean;
19
+ content: string;
20
+ created: Date;
21
+ }
22
+ declare module 'koishi' {
23
+ interface Tables {
24
+ 'maple-drift-bottles': DriftBottle;
25
+ 'maple-drift-bottle-comments': BottleComment;
26
+ }
27
+ }
28
+ export interface Config {
29
+ anonymousByDefault: boolean;
30
+ maxContentLength: number;
31
+ maxPreviewLength: number;
32
+ maxCommentLength: number;
33
+ maxImageSize: number;
34
+ adminQQ: string[];
35
+ }
36
+ export declare const Config: Schema<Config>;
37
+ export declare function apply(ctx: Context, config: Config): void;
38
+ export {};
package/lib/index.js CHANGED
@@ -32,17 +32,19 @@ var using = ["database"];
32
32
  var Config = import_koishi.Schema.object({
33
33
  anonymousByDefault: import_koishi.Schema.boolean().default(false).description("默认是否匿名(true: 匿名,false: 不匿名)"),
34
34
  maxContentLength: import_koishi.Schema.number().default(500).min(50).max(2e3).description("漂流瓶内容最大长度(字)(50-2000)"),
35
- maxPreviewLength: import_koishi.Schema.number().default(8).min(3).max(50).description('指令"我的漂流瓶"最大预览长度(字)(3-50)'),
35
+ maxPreviewLength: import_koishi.Schema.number().default(8).min(3).max(50).description('指令"我的漂流瓶"最大预览字数(3-50)'),
36
+ maxCommentLength: import_koishi.Schema.number().default(50).min(10).max(500).description("漂流瓶评论最大长度(字)(10-500)"),
37
+ maxImageSize: import_koishi.Schema.number().default(1e3).min(0).max(1e4).description("漂流瓶图片大小限制(KB)(0-10000,0表示不允许图片)"),
36
38
  adminQQ: import_koishi.Schema.array(String).role("table").default([]).description("管理人QQ号(可添加多个)")
37
39
  });
38
40
  function getSenderDisplay(bottle) {
39
- if (bottle.anonymous) {
40
- return "匿名";
41
- } else {
42
- return `${bottle.authorName} (${bottle.author})`;
43
- }
41
+ return bottle.anonymous ? "匿名" : bottle.authorName;
44
42
  }
45
43
  __name(getSenderDisplay, "getSenderDisplay");
44
+ function getCommenterDisplay(comment) {
45
+ return comment.anonymous ? "匿名" : comment.authorName;
46
+ }
47
+ __name(getCommenterDisplay, "getCommenterDisplay");
46
48
  function formatTime(date) {
47
49
  return date.toLocaleString("zh-CN", {
48
50
  year: "numeric",
@@ -54,13 +56,24 @@ function formatTime(date) {
54
56
  });
55
57
  }
56
58
  __name(formatTime, "formatTime");
57
- function getContentPreview(content, maxLength) {
58
- if (content.length <= maxLength) {
59
- return content;
59
+ function getContentPreview(content, hasImage, maxLength) {
60
+ let preview = content;
61
+ if (hasImage) {
62
+ preview += "[图片]";
60
63
  }
61
- return content.substring(0, maxLength) + "...";
64
+ if (preview.length <= maxLength) {
65
+ return preview;
66
+ }
67
+ return preview.substring(0, maxLength) + "...";
62
68
  }
63
69
  __name(getContentPreview, "getContentPreview");
70
+ function getBase64ImageSize(base64) {
71
+ const stringLength = base64.length;
72
+ const sizeInBytes = 4 * Math.ceil(stringLength / 3) * 0.5624896334383812;
73
+ const sizeInKB = sizeInBytes / 1024;
74
+ return sizeInKB;
75
+ }
76
+ __name(getBase64ImageSize, "getBase64ImageSize");
64
77
  function apply(ctx, config) {
65
78
  console.log("maple-drift-bottle 漂流瓶插件加载中...");
66
79
  ctx.model.extend("maple-drift-bottles", {
@@ -73,12 +86,35 @@ function apply(ctx, config) {
73
86
  // 是否匿名
74
87
  content: "text",
75
88
  // 漂流瓶内容(text类型,支持长文本)
89
+ image: "text",
90
+ // 图片base64数据
76
91
  created: "timestamp"
77
92
  // 创建时间
78
93
  }, {
79
94
  primary: "id",
80
95
  autoInc: true
81
96
  });
97
+ ctx.model.extend("maple-drift-bottle-comments", {
98
+ id: "unsigned",
99
+ bottleId: "unsigned",
100
+ // 关联的漂流瓶ID
101
+ author: "string",
102
+ // 评论者ID
103
+ authorName: "string",
104
+ // 评论者昵称
105
+ anonymous: "boolean",
106
+ // 是否匿名评论
107
+ content: "text",
108
+ // 评论内容
109
+ created: "timestamp"
110
+ // 评论时间
111
+ }, {
112
+ primary: "id",
113
+ autoInc: true,
114
+ foreign: {
115
+ bottleId: ["maple-drift-bottles", "id"]
116
+ }
117
+ });
82
118
  ctx.command("漂流瓶", "漂流瓶相关指令").action(({ session }) => {
83
119
  return session.execute("help 漂流瓶");
84
120
  });
@@ -90,30 +126,85 @@ function apply(ctx, config) {
90
126
  }
91
127
  const randomIndex = Math.floor(Math.random() * allBottles.length);
92
128
  const bottle = allBottles[randomIndex];
129
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
130
+ bottleId: bottle.id
131
+ });
132
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
93
133
  const senderDisplay = getSenderDisplay(bottle);
94
134
  const timeDisplay = formatTime(bottle.created);
95
- return `【漂流瓶 #${bottle.id}】
135
+ let textContent = bottle.content;
136
+ if (bottle.image) {
137
+ textContent += "\n[图片]";
138
+ }
139
+ let output = `【漂流瓶 #${bottle.id}】
96
140
  发送者:${senderDisplay}
97
141
  时间:${timeDisplay}
98
142
  内容:
99
- ${bottle.content}`;
143
+ ${textContent}`;
144
+ if (comments.length > 0) {
145
+ output += "\n──────────\n最新评论:\n";
146
+ const recentComments = comments.slice(0, 3);
147
+ recentComments.forEach((comment, index) => {
148
+ const commenterDisplay = getCommenterDisplay(comment);
149
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
150
+ `;
151
+ });
152
+ }
153
+ await session.send(output);
154
+ if (bottle.image) {
155
+ try {
156
+ await session.send(`[CQ:image,file=base64://${bottle.image}]`);
157
+ } catch (error) {
158
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
159
+ await session.send("(图片发送失败)");
160
+ }
161
+ }
162
+ return "";
100
163
  } catch (error) {
101
164
  ctx.logger("maple-drift-bottle").error("捞漂流瓶时出错:", error);
102
165
  return "捞取漂流瓶时出错了,请稍后再试。";
103
166
  }
104
167
  });
105
- ctx.command("漂流瓶/扔漂流瓶 <content:text>", "扔一个漂流瓶到海里").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }, content) => {
168
+ ctx.command("漂流瓶/扔漂流瓶 <content:text>", "扔一个漂流瓶到海里(可附带一张图片)").alias("扔漂流瓶").alias("丢漂流瓶").option("invisible", "-i 匿名发送(不显示发送者)").option("visible", "-v 公开发送(显示发送者)").example("扔漂流瓶 这是一条漂流瓶内容").example("扔漂流瓶 -i 这是一条匿名漂流瓶内容").action(async ({ session, options }, content) => {
106
169
  try {
170
+ const hasImage = session.elements?.some((el) => el.type === "image");
107
171
  if (!content || content.trim().length === 0) {
108
- return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
172
+ if (!hasImage) {
173
+ return "漂流瓶内容不能为空!\n格式:扔漂流瓶 [-i|-v] 内容\n示例:扔漂流瓶 -i 这是一条匿名漂流瓶";
174
+ }
109
175
  }
110
- const trimmedContent = content.trim();
176
+ const trimmedContent = content ? content.trim() : "";
111
177
  if (trimmedContent.length > config.maxContentLength) {
112
178
  return `漂流瓶内容太长了!最多只能输入${config.maxContentLength}个字(当前: ${trimmedContent.length})。`;
113
179
  }
114
180
  if (options.invisible && options.visible) {
115
181
  return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
116
182
  }
183
+ let imageBase64 = null;
184
+ if (hasImage) {
185
+ const images = session.elements.filter((el) => el.type === "image");
186
+ if (images.length > 1) {
187
+ return "最多只能发送一张图片!";
188
+ }
189
+ if (config.maxImageSize === 0) {
190
+ return "当前配置不允许发送图片!";
191
+ }
192
+ const imageElement = images[0];
193
+ let imageUrl = imageElement.attrs.url || imageElement.attrs.file;
194
+ if (imageUrl && imageUrl.startsWith("base64://")) {
195
+ imageBase64 = imageUrl.substring(9);
196
+ const imageSize = getBase64ImageSize(imageBase64);
197
+ if (imageSize > config.maxImageSize) {
198
+ return `图片太大了!最大允许 ${config.maxImageSize}KB(当前: ${imageSize.toFixed(2)}KB)。`;
199
+ }
200
+ } else if (imageUrl) {
201
+ try {
202
+ await session.send("正在处理图片,请稍候...");
203
+ } catch (error) {
204
+ return "图片处理失败,请稍后再试。";
205
+ }
206
+ }
207
+ }
117
208
  let anonymous = config.anonymousByDefault;
118
209
  if (options.invisible) {
119
210
  anonymous = true;
@@ -127,12 +218,14 @@ ${bottle.content}`;
127
218
  authorName,
128
219
  anonymous,
129
220
  content: trimmedContent,
221
+ image: imageBase64,
130
222
  created: /* @__PURE__ */ new Date()
131
223
  });
132
224
  const anonymousText = anonymous ? "匿名" : "不匿名";
225
+ const imageText = imageBase64 ? "(含图片)" : "";
133
226
  return `成功扔出一个漂流瓶!
134
227
  ID: #${newBottle.id}
135
- 状态: ${anonymousText}
228
+ 状态: ${anonymousText}${imageText}
136
229
  内容已保存到海中,等待有缘人捞取。`;
137
230
  } catch (error) {
138
231
  ctx.logger("maple-drift-bottle").error("扔漂流瓶时出错:", error);
@@ -166,7 +259,7 @@ ID: #${newBottle.id}
166
259
  const displayIndex = startIndex + index + 1;
167
260
  const timeDisplay = formatTime(bottle.created);
168
261
  const anonymousText = bottle.anonymous ? "(匿名)" : "";
169
- const contentPreview = getContentPreview(bottle.content, config.maxPreviewLength);
262
+ const contentPreview = getContentPreview(bottle.content, !!bottle.image, config.maxPreviewLength);
170
263
  output += `${displayIndex}. 漂流瓶 #${bottle.id} ${anonymousText} - ${timeDisplay}
171
264
  预览: ${contentPreview}
172
265
  `;
@@ -202,14 +295,41 @@ ID: #${newBottle.id}
202
295
  return `仅可查看由你自己发送的漂流瓶。
203
296
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
204
297
  }
298
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
299
+ bottleId: bottle.id
300
+ });
301
+ comments.sort((a, b) => b.created.getTime() - a.created.getTime());
205
302
  const senderDisplay = getSenderDisplay(bottle);
206
303
  const timeDisplay = formatTime(bottle.created);
207
- return `【漂流瓶 #${bottle.id}】
304
+ let textContent = bottle.content;
305
+ if (bottle.image) {
306
+ textContent += "\n[图片]";
307
+ }
308
+ let output = `【漂流瓶 #${bottle.id}】
208
309
  发送者:${senderDisplay}
209
310
  时间:${timeDisplay}
210
311
  状态:${bottle.anonymous ? "匿名" : "公开"}
211
312
  内容:
212
- ${bottle.content}`;
313
+ ${textContent}`;
314
+ if (comments.length > 0) {
315
+ output += "\n──────────\n最新评论:\n";
316
+ const recentComments = comments.slice(0, 3);
317
+ recentComments.forEach((comment, index) => {
318
+ const commenterDisplay = getCommenterDisplay(comment);
319
+ output += `${index + 1}. ${commenterDisplay}:${comment.content}
320
+ `;
321
+ });
322
+ }
323
+ await session.send(output);
324
+ if (bottle.image) {
325
+ try {
326
+ await session.send(`[CQ:image,file=base64://${bottle.image}]`);
327
+ } catch (error) {
328
+ ctx.logger("maple-drift-bottle").error("发送图片时出错:", error);
329
+ await session.send("(图片发送失败)");
330
+ }
331
+ }
332
+ return "";
213
333
  } catch (error) {
214
334
  ctx.logger("maple-drift-bottle").error("查看漂流瓶时出错:", error);
215
335
  return "查看漂流瓶时出错了,请稍后再试。";
@@ -230,19 +350,130 @@ ${bottle.content}`;
230
350
  return `仅可删除由你自己发送的漂流瓶。
231
351
  可以发送"我的漂流瓶"查看你发送的所有漂流瓶序号。`;
232
352
  }
233
- const senderDisplay = getSenderDisplay(bottle);
234
- const timeDisplay = formatTime(bottle.created);
235
- const contentPreview = bottle.content.length > 50 ? bottle.content.substring(0, 50) + "..." : bottle.content;
353
+ await ctx.database.remove("maple-drift-bottle-comments", {
354
+ bottleId: id
355
+ });
236
356
  await ctx.database.remove("maple-drift-bottles", { id });
237
- return `已成功删除漂流瓶 #${id}
238
- 发送者:${senderDisplay}
239
- 时间:${timeDisplay}
240
- 预览:${contentPreview}`;
357
+ return `已成功删除漂流瓶 #${id},并同时删除了该漂流瓶的所有评论。`;
241
358
  } catch (error) {
242
359
  ctx.logger("maple-drift-bottle").error("删除漂流瓶时出错:", error);
243
360
  return "删除漂流瓶时出错了,请稍后再试。";
244
361
  }
245
362
  });
363
+ ctx.command("漂流瓶/评论漂流瓶 <bottleId:number> <content:text>", "对指定的漂流瓶进行评论").alias("评论漂流瓶").option("invisible", "-i 匿名评论(不显示评论者)").option("visible", "-v 公开评论(显示评论者)").example("评论漂流瓶 1 这是一条评论").example("评论漂流瓶 2 -i 这是一条匿名评论").action(async ({ session, options }, bottleId, content) => {
364
+ try {
365
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
366
+ if (bottles.length === 0) {
367
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
368
+ }
369
+ if (!content || content.trim().length === 0) {
370
+ return "评论内容不能为空!";
371
+ }
372
+ const trimmedContent = content.trim();
373
+ if (trimmedContent.length > config.maxCommentLength) {
374
+ return `评论内容太长了!最多只能输入${config.maxCommentLength}个字(当前: ${trimmedContent.length})。`;
375
+ }
376
+ if (options.invisible && options.visible) {
377
+ return "选项冲突:-i(匿名)和-v(公开)不能同时使用";
378
+ }
379
+ let anonymous = config.anonymousByDefault;
380
+ if (options.invisible) {
381
+ anonymous = true;
382
+ } else if (options.visible) {
383
+ anonymous = false;
384
+ }
385
+ const userInfo = await session.getUser(session.userId);
386
+ const authorName = userInfo?.name || session.username || `用户${session.userId}`;
387
+ await ctx.database.create("maple-drift-bottle-comments", {
388
+ bottleId,
389
+ author: session.userId,
390
+ authorName,
391
+ anonymous,
392
+ content: trimmedContent,
393
+ created: /* @__PURE__ */ new Date()
394
+ });
395
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
396
+ bottleId
397
+ });
398
+ if (comments.length > 3) {
399
+ comments.sort((a, b) => a.created.getTime() - b.created.getTime());
400
+ const commentsToDelete = comments.slice(0, comments.length - 3);
401
+ for (const comment of commentsToDelete) {
402
+ await ctx.database.remove("maple-drift-bottle-comments", { id: comment.id });
403
+ }
404
+ }
405
+ const anonymousText = anonymous ? "匿名" : "公开";
406
+ return `成功评论漂流瓶 #${bottleId}!
407
+ 状态: ${anonymousText}
408
+ 评论已保存。`;
409
+ } catch (error) {
410
+ ctx.logger("maple-drift-bottle").error("评论漂流瓶时出错:", error);
411
+ return "评论漂流瓶时出错了,请稍后再试。";
412
+ }
413
+ });
414
+ ctx.command("漂流瓶/删除漂流瓶评论 <bottleId:number>", "删除指定漂流瓶的所有评论").alias("删除漂流瓶评论").option("all", "-a 无视权限限制删除评论").example("删除漂流瓶评论 1").action(async ({ session, options }, bottleId) => {
415
+ try {
416
+ const bottles = await ctx.database.get("maple-drift-bottles", { id: bottleId });
417
+ if (bottles.length === 0) {
418
+ return `没有找到ID为 #${bottleId} 的漂流瓶。`;
419
+ }
420
+ const bottle = bottles[0];
421
+ const comments = await ctx.database.get("maple-drift-bottle-comments", {
422
+ bottleId
423
+ });
424
+ if (comments.length === 0) {
425
+ return `漂流瓶 #${bottleId} 还没有任何评论。`;
426
+ }
427
+ if (!options.all) {
428
+ const userId = session.userId;
429
+ const isBottleAuthor = bottle.author === userId;
430
+ const userComments = comments.filter((comment) => comment.author === userId);
431
+ const isCommentAuthor = userComments.length > 0;
432
+ if (!isBottleAuthor && !isCommentAuthor) {
433
+ return `仅可由漂流瓶发送者或评论发送者删除该漂流瓶的评论。
434
+ 可以发送"我的漂流瓶"查看你发送的漂流瓶序号。`;
435
+ }
436
+ if (!isBottleAuthor && isCommentAuthor) {
437
+ await ctx.database.remove("maple-drift-bottle-comments", {
438
+ bottleId,
439
+ author: userId
440
+ });
441
+ const deletedCount = userComments.length;
442
+ return `已成功删除你在漂流瓶 #${bottleId} 的评论(共 ${deletedCount} 条)。`;
443
+ }
444
+ }
445
+ await ctx.database.remove("maple-drift-bottle-comments", {
446
+ bottleId
447
+ });
448
+ return `已成功删除漂流瓶 #${bottleId} 的所有评论(共 ${comments.length} 条)。`;
449
+ } catch (error) {
450
+ ctx.logger("maple-drift-bottle").error("删除漂流瓶评论时出错:", error);
451
+ return "删除漂流瓶评论时出错了,请稍后再试。";
452
+ }
453
+ });
454
+ ctx.command("漂流瓶/清空漂流瓶评论 [options]", "清空漂流瓶评论").alias("清空漂流瓶评论").option("all", "-a 清空所有漂流瓶评论(默认)").option("user", "-u <userId:string> 清空指定用户的所有漂流瓶评论").example("清空漂流瓶评论").example("清空漂流瓶评论 -u 123456").action(async ({ session, options }) => {
455
+ try {
456
+ const query = {};
457
+ let description = "";
458
+ if (options.user) {
459
+ query.author = options.user;
460
+ description += `用户 ${options.user} 的`;
461
+ }
462
+ if (!options.user) {
463
+ description = "所有";
464
+ }
465
+ const comments = await ctx.database.get("maple-drift-bottle-comments", query);
466
+ const count = comments.length;
467
+ if (count === 0) {
468
+ return `没有找到${description}漂流瓶评论。`;
469
+ }
470
+ await ctx.database.remove("maple-drift-bottle-comments", query);
471
+ return `已成功清空 ${count} 条${description}漂流瓶评论。`;
472
+ } catch (error) {
473
+ ctx.logger("maple-drift-bottle").error("清空漂流瓶评论时出错:", error);
474
+ return "清空漂流瓶评论时出错了,请稍后再试。";
475
+ }
476
+ });
246
477
  ctx.command("漂流瓶/清空漂流瓶 [options]", "清空漂流瓶").alias("清空漂流瓶").alias("清空瓶子").option("all", "-a 清空所有漂流瓶(默认)").option("user", "-u <userId:string> 清空指定用户的所有漂流瓶").option("date", "-d <date:string> 清空指定日期之前的漂流瓶(格式: yyyy-mm-dd)").example("清空漂流瓶").example("清空漂流瓶 -d 2023-12-31").action(async ({ session, options }) => {
247
478
  try {
248
479
  const query = {};
@@ -273,8 +504,13 @@ ${bottle.content}`;
273
504
  if (count === 0) {
274
505
  return `没有找到${description}漂流瓶。`;
275
506
  }
507
+ for (const bottle of bottles) {
508
+ await ctx.database.remove("maple-drift-bottle-comments", {
509
+ bottleId: bottle.id
510
+ });
511
+ }
276
512
  await ctx.database.remove("maple-drift-bottles", query);
277
- return `已成功清空 ${count} 条${description}漂流瓶记录。`;
513
+ return `已成功清空 ${count} 条${description}漂流瓶记录,并同时删除了相关评论。`;
278
514
  } catch (error) {
279
515
  ctx.logger("maple-drift-bottle").error("清空漂流瓶时出错:", error);
280
516
  return "清空漂流瓶时出错了,请稍后再试。";
@@ -332,6 +568,8 @@ ${failDetails.join("\n")}`;
332
568
  ctx.logger("maple-drift-bottle").info(`默认匿名设置: ${config.anonymousByDefault ? "是" : "否"}`);
333
569
  ctx.logger("maple-drift-bottle").info(`内容最大长度: ${config.maxContentLength} 字`);
334
570
  ctx.logger("maple-drift-bottle").info(`我的漂流瓶最大预览字数: ${config.maxPreviewLength} 字`);
571
+ ctx.logger("maple-drift-bottle").info(`漂流瓶评论最大长度: ${config.maxCommentLength} 字`);
572
+ ctx.logger("maple-drift-bottle").info(`漂流瓶图片大小限制: ${config.maxImageSize} KB`);
335
573
  ctx.logger("maple-drift-bottle").info(`管理人QQ: ${config.adminQQ.length > 0 ? config.adminQQ.join(", ") : "未配置"}`);
336
574
  });
337
575
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-maple-drift-bottle",
3
3
  "description": "-",
4
- "version": "0.0.3",
4
+ "version": "0.1.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [