koishi-plugin-memesluna 0.4.5 → 0.4.7

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.js CHANGED
@@ -36,7 +36,8 @@ __export(index_exports, {
36
36
  hashImageBuffer: () => hashImageBuffer,
37
37
  inject: () => inject,
38
38
  isReservedPath: () => isReservedPath,
39
- name: () => name
39
+ name: () => name,
40
+ sanitizeFilename: () => sanitizeFilename
40
41
  });
41
42
  module.exports = __toCommonJS(index_exports);
42
43
  var import_fs = require("fs");
@@ -48,6 +49,13 @@ var import_crypto = require("crypto");
48
49
  var import_promises = __toESM(require("fs/promises"));
49
50
  var import_path = __toESM(require("path"));
50
51
  var import_koishi = require("koishi");
52
+ function sanitizeFilename(filename) {
53
+ const ext = import_path.default.extname(filename).toLowerCase();
54
+ const base = import_path.default.basename(filename, ext);
55
+ const safeBase = base.replace(/[\s/\\?%*:|"<>,;=@]/g, "_");
56
+ return `${safeBase}${ext}`;
57
+ }
58
+ __name(sanitizeFilename, "sanitizeFilename");
51
59
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
52
60
  ".jpg",
53
61
  ".jpeg",
@@ -213,17 +221,6 @@ var MemesLunaService = class extends import_koishi.Service {
213
221
  indexes: ["hash"]
214
222
  }
215
223
  );
216
- this.ctx.database.extend(
217
- "memesluna_collection_stats",
218
- {
219
- collection: "string",
220
- api_call_count: "integer",
221
- updated_at: "timestamp"
222
- },
223
- {
224
- primary: "collection"
225
- }
226
- );
227
224
  this.ctx.database.extend(
228
225
  "memesluna_staged_images",
229
226
  {
@@ -276,55 +273,42 @@ var MemesLunaService = class extends import_koishi.Service {
276
273
  return;
277
274
  }
278
275
  const existingImages = await this.ctx.database.get("memesluna_images", { collection: colName });
279
- const existingIndices = new Set(existingImages.map((img) => img.index));
276
+ const existingFilenames = new Set(existingImages.map((img) => img.filename));
280
277
  const existingExternalValues = new Set(
281
278
  existingImages.filter((img) => img.type === "external").map((img) => img.value)
282
279
  );
280
+ const existingIndices = new Set(existingImages.map((img) => img.index));
283
281
  let maxIndex = existingImages.reduce((max, img) => Math.max(max, img.index), 0);
284
282
  for (const filename of files) {
285
- const ext = import_path.default.extname(filename).toLowerCase();
286
- const basename = import_path.default.basename(filename, ext);
287
- let index = parseInt(basename, 10);
288
- if (isNaN(index) || String(index) !== basename || index < 1 || index > 99999) {
289
- index = ++maxIndex;
290
- const newFilename = `${index}${ext}`;
283
+ const safeName = sanitizeFilename(filename);
284
+ let currentName = filename;
285
+ if (safeName !== filename) {
291
286
  const oldPath = import_path.default.join(colDir, filename);
292
- const newPath = import_path.default.join(colDir, newFilename);
287
+ const newPath = import_path.default.join(colDir, safeName);
293
288
  try {
294
289
  await import_promises.default.rename(oldPath, newPath);
290
+ currentName = safeName;
291
+ } catch {
292
+ }
293
+ }
294
+ if (!existingFilenames.has(currentName)) {
295
+ const index = ++maxIndex;
296
+ const fullPath = import_path.default.join(colDir, currentName);
297
+ try {
295
298
  await this.ctx.database.create("memesluna_images", {
296
299
  id: (0, import_crypto.randomUUID)(),
297
300
  collection: colName,
298
301
  index,
299
- filename: newFilename,
302
+ filename: currentName,
300
303
  type: "local",
301
- value: newFilename,
302
- mime: this.getMimeByFilename(newFilename),
303
- ...await this.getImageFingerprints(await import_promises.default.readFile(newPath)),
304
+ value: currentName,
305
+ mime: this.getMimeByFilename(currentName),
306
+ ...await this.getImageFingerprints(await import_promises.default.readFile(fullPath)),
304
307
  created_at: /* @__PURE__ */ new Date()
305
308
  });
306
- existingIndices.add(index);
309
+ existingFilenames.add(currentName);
307
310
  } catch {
308
311
  }
309
- } else {
310
- if (!existingIndices.has(index)) {
311
- try {
312
- await this.ctx.database.create("memesluna_images", {
313
- id: (0, import_crypto.randomUUID)(),
314
- collection: colName,
315
- index,
316
- filename,
317
- type: "local",
318
- value: filename,
319
- mime: this.getMimeByFilename(filename),
320
- ...await this.getImageFingerprints(await import_promises.default.readFile(import_path.default.join(colDir, filename))),
321
- created_at: /* @__PURE__ */ new Date()
322
- });
323
- existingIndices.add(index);
324
- maxIndex = Math.max(maxIndex, index);
325
- } catch {
326
- }
327
- }
328
312
  }
329
313
  }
330
314
  const linksFile = this.getCollectionLinksFile(colName);
@@ -854,6 +838,19 @@ var MemesLunaService = class extends import_koishi.Service {
854
838
  }
855
839
  }
856
840
  }
841
+ async deduplicateDatabaseFilename(collectionName, filename) {
842
+ const parsed = import_path.default.parse(filename);
843
+ let counter = 1;
844
+ let candidate = filename;
845
+ while (true) {
846
+ const rows = await this.ctx.database.get("memesluna_images", { collection: collectionName, filename: candidate });
847
+ if (!rows.length) {
848
+ return candidate;
849
+ }
850
+ candidate = `${parsed.name}_${counter}${parsed.ext}`;
851
+ counter++;
852
+ }
853
+ }
857
854
  async addLocalImageBuffer(collectionName, buffer, originalName, extHint) {
858
855
  this.ensureCollectionName(collectionName);
859
856
  if (!await this.collectionExists(collectionName)) {
@@ -874,7 +871,9 @@ var MemesLunaService = class extends import_koishi.Service {
874
871
  if (!IMAGE_EXTENSIONS.has(finalExt)) {
875
872
  throw new Error("Unsupported image format");
876
873
  }
877
- const finalName = `${index}${finalExt}`;
874
+ const baseName = originalName ? import_path.default.basename(originalName, import_path.default.extname(originalName)) : `image-${Date.now()}`;
875
+ const safeBase = sanitizeFilename(`${baseName}${finalExt}`);
876
+ const finalName = await this.deduplicateDatabaseFilename(collectionName, safeBase);
878
877
  const backend = this.getStorageBackend();
879
878
  if (backend) {
880
879
  const result = await backend.upload(buffer, finalName);
@@ -1107,7 +1106,6 @@ var MemesLunaService = class extends import_koishi.Service {
1107
1106
  }
1108
1107
  const createdAt = dates.length ? new Date(Math.min(...dates.map((date) => date.getTime()), statCreatedAt?.getTime() || Infinity)) : statCreatedAt;
1109
1108
  const updatedAt = dates.length ? new Date(Math.max(...dates.map((date) => date.getTime()), statUpdatedAt?.getTime() || 0)) : statUpdatedAt;
1110
- const apiCallCount = await this.getCollectionApiCallCount(collectionName);
1111
1109
  return {
1112
1110
  name: collectionName,
1113
1111
  description,
@@ -1117,36 +1115,9 @@ var MemesLunaService = class extends import_koishi.Service {
1117
1115
  hasContent: localImages.length > 0 || links.length > 0,
1118
1116
  createdAt,
1119
1117
  updatedAt,
1120
- apiCallCount,
1121
1118
  cover: localImages[0]
1122
1119
  };
1123
1120
  }
1124
- async getCollectionApiCallCount(collectionName) {
1125
- if (!this.isValidCollectionName(collectionName)) {
1126
- return 0;
1127
- }
1128
- const rows = await this.ctx.database.get("memesluna_collection_stats", { collection: collectionName });
1129
- return rows[0]?.api_call_count || 0;
1130
- }
1131
- async incrementCollectionApiCallCount(collectionName) {
1132
- if (!this.isValidCollectionName(collectionName)) {
1133
- return;
1134
- }
1135
- const rows = await this.ctx.database.get("memesluna_collection_stats", { collection: collectionName });
1136
- const now = /* @__PURE__ */ new Date();
1137
- if (rows.length) {
1138
- await this.ctx.database.set("memesluna_collection_stats", { collection: collectionName }, {
1139
- api_call_count: (rows[0].api_call_count || 0) + 1,
1140
- updated_at: now
1141
- });
1142
- return;
1143
- }
1144
- await this.ctx.database.create("memesluna_collection_stats", {
1145
- collection: collectionName,
1146
- api_call_count: 1,
1147
- updated_at: now
1148
- });
1149
- }
1150
1121
  async getRandomResource(collectionName) {
1151
1122
  if (!this.isValidCollectionName(collectionName)) {
1152
1123
  return null;
@@ -1366,7 +1337,6 @@ async function applyDynamicForward(ctx, config, service, routeName, _query, requ
1366
1337
  if (!resource) {
1367
1338
  return { notFound: true };
1368
1339
  }
1369
- await service.incrementCollectionApiCallCount(routeName);
1370
1340
  if (resource.type === "external") {
1371
1341
  return { redirectTo: resource.value };
1372
1342
  }
@@ -1988,6 +1958,24 @@ function applyServer(ctx, config, service) {
1988
1958
  );
1989
1959
  setKoaResponse(koa, result);
1990
1960
  });
1961
+ ctx.server.get(`${basePath}/:name/:filename`, async (koa) => {
1962
+ const collectionName = toTrimmedString(koa.params.name);
1963
+ const filename = toTrimmedString(koa.params.filename);
1964
+ if (isReservedPath(collectionName)) {
1965
+ koa.status = 404;
1966
+ koa.body = { error: "Not Found" };
1967
+ return;
1968
+ }
1969
+ const image = await service.getLocalImageBuffer(collectionName, filename);
1970
+ if (!image) {
1971
+ koa.status = 404;
1972
+ koa.body = { error: "Image not found" };
1973
+ return;
1974
+ }
1975
+ koa.status = 200;
1976
+ koa.set("Content-Type", image.mime);
1977
+ koa.body = image.buffer;
1978
+ });
1991
1979
  }
1992
1980
  __name(applyServer, "applyServer");
1993
1981
  function apply(ctx, config) {
@@ -2122,5 +2110,6 @@ var inject = {
2122
2110
  hashImageBuffer,
2123
2111
  inject,
2124
2112
  isReservedPath,
2125
- name
2113
+ name,
2114
+ sanitizeFilename
2126
2115
  });
package/lib/service.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Context, Service } from 'koishi';
2
2
  import type { Config } from './config';
3
+ export declare function sanitizeFilename(filename: string): string;
3
4
  export declare function hashImageBuffer(buffer: Buffer): string;
4
5
  export declare function isReservedPath(name: string): boolean;
5
6
  export interface ApiEndpoint {
@@ -28,7 +29,6 @@ export interface CollectionInfo {
28
29
  hasContent: boolean;
29
30
  createdAt?: Date;
30
31
  updatedAt?: Date;
31
- apiCallCount: number;
32
32
  cover?: string;
33
33
  }
34
34
  export interface CollectionResource {
@@ -118,6 +118,7 @@ export declare class MemesLunaService extends Service {
118
118
  private buildStagedFilename;
119
119
  private resolveStagedImagePath;
120
120
  private deduplicateFilename;
121
+ private deduplicateDatabaseFilename;
121
122
  addLocalImageBuffer(collectionName: string, buffer: Buffer, originalName?: string, extHint?: string): Promise<string>;
122
123
  addLocalImageBase64(collectionName: string, base64Data: string, originalName?: string): Promise<string>;
123
124
  addStagedImageBuffer(buffer: Buffer, originalName?: string, source?: string, reason?: string): Promise<StagedImageInfo>;
@@ -133,8 +134,6 @@ export declare class MemesLunaService extends Service {
133
134
  deleteImageFromCollection(collectionName: string, filename: string): Promise<boolean>;
134
135
  moveImageToCollection(sourceCollection: string, targetCollection: string, filename: string): Promise<string | null>;
135
136
  getCollectionInfo(collectionName: string): Promise<CollectionInfo | null>;
136
- getCollectionApiCallCount(collectionName: string): Promise<number>;
137
- incrementCollectionApiCallCount(collectionName: string): Promise<void>;
138
137
  getRandomResource(collectionName: string): Promise<CollectionResource | null>;
139
138
  private mapEndpoint;
140
139
  getEndpoints(): Promise<ApiEndpoint[]>;
@@ -177,11 +176,6 @@ declare module 'koishi' {
177
176
  perceptual_hash: string;
178
177
  created_at: Date;
179
178
  };
180
- memesluna_collection_stats: {
181
- collection: string;
182
- api_call_count: number;
183
- updated_at: Date;
184
- };
185
179
  memesluna_staged_images: {
186
180
  id: string;
187
181
  filename: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-memesluna",
3
3
  "description": "Image Forward service for Koishi with ChatLuna integration",
4
- "version": "0.4.5",
4
+ "version": "0.4.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",