koishi-plugin-nitter 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.
@@ -0,0 +1,2 @@
1
+ export declare function downloadImagesToBase64(imageUrls: any): Promise<any[]>;
2
+ export declare function downloadVideosToBase64(videoUrls: any, maxSize?: number): Promise<any[]>;
@@ -0,0 +1,7 @@
1
+ export declare class FixedSet<T> {
2
+ private maxSize;
3
+ private set;
4
+ constructor(maxSize: number);
5
+ add(element: T): void;
6
+ has(element: T): boolean;
7
+ }
package/lib/index.d.ts CHANGED
@@ -16,8 +16,16 @@ export interface Config {
16
16
  app: string;
17
17
  enableReTweet: boolean;
18
18
  sendPic: boolean;
19
+ cronString: string;
19
20
  maxSize: number;
20
21
  }
22
+ declare module 'koishi' {
23
+ interface Tables {
24
+ nitter_records: {
25
+ id: string;
26
+ createdAt: Date;
27
+ };
28
+ }
29
+ }
21
30
  export declare const Config: Schema<Config, Dict>;
22
31
  export declare function apply(ctx: Context, config: Config): void;
23
- export declare function downloadVideosToBase64(videoUrls: any, maxSize?: number): Promise<any[]>;
package/lib/index.js CHANGED
@@ -32,7 +32,6 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
- downloadVideosToBase64: () => downloadVideosToBase64,
36
35
  inject: () => inject,
37
36
  name: () => name
38
37
  });
@@ -42,18 +41,21 @@ var import_koishi = require("koishi");
42
41
  // src/translate.ts
43
42
  var import_axios = __toESM(require("axios"));
44
43
  var import_https_proxy_agent = require("https-proxy-agent");
44
+
45
+ // src/retry.ts
45
46
  async function retry(retries, fn, delay = 500) {
46
47
  try {
47
- await fn();
48
- return Promise.resolve();
48
+ return await fn();
49
49
  } catch (err) {
50
50
  console.log(`剩余${retries}次尝试`, err);
51
- await new Promise((resolve) => setTimeout(resolve, delay));
52
- return retries > 1 ? retry(retries - 1, fn, delay * 2) : Promise.reject(err);
51
+ if (retries <= 1) throw err;
52
+ await new Promise((r) => setTimeout(r, delay));
53
+ return retry(retries - 1, fn, delay * 2);
53
54
  }
54
- ;
55
55
  }
56
56
  __name(retry, "retry");
57
+
58
+ // src/translate.ts
57
59
  var translate;
58
60
  function setGoogleTranslate(key, proxy) {
59
61
  translate = /* @__PURE__ */ __name((texts) => {
@@ -109,6 +111,7 @@ json输出格式:["翻译结果1", "翻译结果2", ...]`
109
111
  });
110
112
  if (response.data && response.data.choices && response.data.choices[0]) {
111
113
  const content = response.data.choices[0].message.content;
114
+ console.log(content);
112
115
  data = JSON.parse(content);
113
116
  if (!Array.isArray(data)) {
114
117
  if (typeof data === "object" && Object.keys(data).length === 1) {
@@ -122,7 +125,7 @@ json输出格式:["翻译结果1", "翻译结果2", ...]`
122
125
  throw new Error("API返回数据格式异常");
123
126
  }
124
127
  });
125
- return data;
128
+ return data.map((content) => content.replace(/\\n/g, "\n"));
126
129
  }, "translate");
127
130
  }
128
131
  __name(setOpenAiTranslate, "setOpenAiTranslate");
@@ -142,8 +145,8 @@ async function addTranslate(page, className) {
142
145
  tempDiv.innerHTML = translatedHTMLs2[index];
143
146
  const newElement = tempDiv.firstElementChild;
144
147
  if (newElement) {
145
- element.insertAdjacentElement("afterend", newElement);
146
- element.insertAdjacentHTML("afterend", '<div style="height: 1px;background-color: #ccc; margin-top: 10px; margin-bottom: 10px;"></div>');
148
+ element.insertAdjacentElement("beforebegin", newElement);
149
+ element.insertAdjacentHTML("beforebegin", '<div style="height: 1px;background-color: #ccc; margin-top: 10px; margin-bottom: 10px;"></div>');
147
150
  }
148
151
  }
149
152
  });
@@ -153,14 +156,87 @@ __name(addTranslate, "addTranslate");
153
156
 
154
157
  // src/index.tsx
155
158
  var import_rettiwt_api = require("rettiwt-api");
156
- var import_node_cron = require("node-cron");
159
+
160
+ // src/download.ts
157
161
  var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
158
162
  var import_path = __toESM(require("path"));
159
163
  var import_promises = __toESM(require("fs/promises"));
164
+ var import_axios2 = __toESM(require("axios"));
165
+ var tempDir = import_path.default.join(process.cwd(), "tmp");
166
+ async function downloadVideosToBase64(videoUrls, maxSize = 20) {
167
+ await import_promises.default.mkdir(tempDir, { recursive: true });
168
+ const results = [];
169
+ for (const url of videoUrls) {
170
+ let outputPath = null;
171
+ try {
172
+ outputPath = import_path.default.join(tempDir, `video_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.mp4`);
173
+ await new Promise((resolve, reject) => {
174
+ (0, import_fluent_ffmpeg.default)(url).inputOptions([
175
+ "-protocol_whitelist",
176
+ "file,http,https,tcp,tls,crypto"
177
+ ]).outputOptions([
178
+ "-c",
179
+ "copy"
180
+ ]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
181
+ });
182
+ const stats = await import_promises.default.stat(outputPath);
183
+ const fileSizeInMB = stats.size / (1024 * 1024);
184
+ if (fileSizeInMB > maxSize) {
185
+ console.log(`视频大小 ${fileSizeInMB.toFixed(2)}MB 超过${maxSize}MB限制,跳过转换`);
186
+ } else {
187
+ const fileBuffer = await import_promises.default.readFile(outputPath);
188
+ const base64String = fileBuffer.toString("base64");
189
+ const dataUrl = `data:video/mp4;base64,${base64String}`;
190
+ results.push(dataUrl);
191
+ }
192
+ } catch (error) {
193
+ console.error(`处理失败 (${url}):`, error.message);
194
+ results.push(null);
195
+ } finally {
196
+ if (outputPath) {
197
+ try {
198
+ await import_promises.default.unlink(outputPath);
199
+ } catch (deleteError) {
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return results;
205
+ }
206
+ __name(downloadVideosToBase64, "downloadVideosToBase64");
207
+
208
+ // src/taskqueue.ts
209
+ var taskQueue = class {
210
+ static {
211
+ __name(this, "taskQueue");
212
+ }
213
+ queue = [];
214
+ consumer;
215
+ processing = false;
216
+ push(task) {
217
+ this.queue.push(task);
218
+ if (!this.processing) {
219
+ this.process();
220
+ }
221
+ }
222
+ onProcess(handler) {
223
+ this.consumer = handler;
224
+ }
225
+ async process() {
226
+ if (this.processing || !this.consumer) return;
227
+ this.processing = true;
228
+ while (this.queue.length > 0) {
229
+ const task = this.queue.shift();
230
+ await this.consumer(task);
231
+ }
232
+ this.processing = false;
233
+ }
234
+ };
235
+
236
+ // src/index.tsx
160
237
  var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
161
238
  var name = "nitter";
162
- var logger = new import_koishi.Logger(name);
163
- var inject = ["puppeteer", "subscription"];
239
+ var inject = ["puppeteer", "subscription", "database"];
164
240
  var Config = import_koishi.Schema.intersect([
165
241
  import_koishi.Schema.object({
166
242
  apiKey: import_koishi.Schema.string().required().description("Twitter API Key"),
@@ -195,23 +271,32 @@ var Config = import_koishi.Schema.intersect([
195
271
  }).description("订阅配置"),
196
272
  import_koishi.Schema.object({
197
273
  enableReTweet: import_koishi.Schema.boolean().default(false).description("是否发送转推"),
198
- sendPic: import_koishi.Schema.boolean().default(false).description("是否单独发送推文中的图片")
274
+ sendPic: import_koishi.Schema.boolean().default(false).description("是否单独发送推文中的图片"),
275
+ cronString: import_koishi.Schema.string().default("15 */5 * * * *").description("使用cron表达式描述检查更新的时间,默认为每隔5分钟检查一次")
199
276
  }).description("推送设置"),
200
277
  import_koishi.Schema.union([
201
278
  import_koishi.Schema.object({
202
279
  sendPic: import_koishi.Schema.const(true).required(),
203
- maxSize: import_koishi.Schema.number().default(10).description("发送视频的最大大小,单位为mb")
280
+ maxSize: import_koishi.Schema.number().default(20).description("发送视频的最大大小,单位为mb")
204
281
  })
205
282
  ])
206
283
  ]);
207
284
  function apply(ctx, config) {
285
+ ctx.model.extend("nitter_records", {
286
+ id: "string",
287
+ createdAt: "timestamp"
288
+ }, {
289
+ primary: "id",
290
+ autoInc: false
291
+ });
208
292
  config.nitterUrl = config.nitterUrl.replace(/\/+$/, "");
209
293
  const twitterClient = new import_rettiwt_api.Rettiwt({
210
294
  apiKey: config.apiKey,
211
- proxyUrl: config.proxy ? new URL(config.proxy) : void 0
295
+ proxyUrl: config.proxy ? new URL(config.proxy) : void 0,
296
+ logging: true
212
297
  });
213
- let latestTweetId;
214
298
  let cronJob;
299
+ const queue = new taskQueue();
215
300
  (async () => {
216
301
  if (config.enableTranslate == "google") {
217
302
  setGoogleTranslate(config.googleApiKey, config.proxy);
@@ -219,16 +304,13 @@ function apply(ctx, config) {
219
304
  setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
220
305
  }
221
306
  const tweetList = await getFollowedFeed();
222
- for (const data of tweetList) {
223
- latestTweetId ||= "";
224
- if (latestTweetId < data.id)
225
- latestTweetId = data.id;
226
- }
227
- cronJob = (0, import_node_cron.schedule)("15 * * * * *", checkForUpdates);
228
- ctx.on("dispose", () => {
229
- cronJob.stop();
230
- });
231
- ctx.logger("nitter").info("开始监听推特动态");
307
+ await ctx.database.upsert("nitter_records", tweetList.map((data) => {
308
+ return {
309
+ id: data.id,
310
+ createdAt: new Date(data.createdAt)
311
+ };
312
+ }));
313
+ queue.onProcess(broadcast);
232
314
  })();
233
315
  ctx.command("nitter.follow", "按照subscription中的订阅配置,使用登录的推特账号关注所有需要订阅的账号", { authority: 3 }).action(async () => {
234
316
  const whiteList = ctx.subscription.getAvailableAccounts(config.app);
@@ -236,9 +318,14 @@ function apply(ctx, config) {
236
318
  const followingIdList = followingList.map((user) => user.userName);
237
319
  for (const id of whiteList) {
238
320
  if (!followingIdList.includes(id)) {
239
- const user = await twitterClient.user.details(id);
321
+ let user;
322
+ await retry(3, async () => {
323
+ user = await twitterClient.user.details(id);
324
+ });
240
325
  await new Promise((resolve) => setTimeout(resolve, 3 * 1e3));
241
- await twitterClient.user.follow(user.id);
326
+ await retry(3, async () => {
327
+ await twitterClient.user.follow(user.id);
328
+ }, 30 * 1e3);
242
329
  console.log(`关注${id}成功`);
243
330
  await new Promise((resolve) => setTimeout(resolve, 30 * 1e3));
244
331
  }
@@ -250,7 +337,7 @@ function apply(ctx, config) {
250
337
  return "请输入推文ID";
251
338
  }
252
339
  try {
253
- const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
340
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(tweetId);
254
341
  let msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
255
342
  let forwardMsg = [];
256
343
  const videoUrls = await downloadVideosToBase64(hlsUrls, config.maxSize);
@@ -270,14 +357,15 @@ function apply(ctx, config) {
270
357
  if (!tweetId) {
271
358
  return "请输入推文ID";
272
359
  }
273
- broadcast(account, tweetId);
360
+ queue.push({ account, tweetId });
274
361
  });
275
- async function broadcast(account, tweetId) {
362
+ async function broadcast(task) {
363
+ const { account, tweetId } = task;
276
364
  try {
277
- const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
365
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(tweetId);
278
366
  const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
279
- ctx.subscription.broadcast(config.app, account, screenshotMsg);
280
- let forwardMsg = [];
367
+ await ctx.subscription.broadcast(config.app, account, screenshotMsg);
368
+ let forwardMsg = [`${ctx.subscription.getName(config.app, account)} ${ctx.subscription.getAccount(config.app, account)}`];
281
369
  const videoUrls = await downloadVideosToBase64(hlsUrls, config.maxSize);
282
370
  if (imageUrls.length > 0) {
283
371
  forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
@@ -306,117 +394,106 @@ function apply(ctx, config) {
306
394
  for (const data of tweetList) {
307
395
  if (!config.enableReTweet && data.retweetedTweet)
308
396
  continue;
309
- if (data.id <= latestTweetId)
397
+ if ((/* @__PURE__ */ new Date()).getTime() - new Date(data.createdAt).getTime() > 2 * 60 * 60 * 1e3)
398
+ continue;
399
+ if ((await ctx.database.get("nitter_records", { id: data.id })).length > 0)
310
400
  continue;
401
+ await ctx.database.upsert("nitter_records", [{
402
+ id: data.id,
403
+ createdAt: new Date(data.createdAt)
404
+ }]);
311
405
  if (!ctx.subscription.getAvailableAccounts(config.app).includes(data.tweetBy.userName))
312
406
  continue;
313
- latestTweetId = data.id;
314
407
  ctx.logger("nitter").info(`检测到推文id:${data.id},开始推送`);
315
- await broadcast(data.tweetBy.userName, data.id);
408
+ queue.push({ account: data.tweetBy.userName, tweetId: data.id });
316
409
  }
317
410
  }
318
411
  __name(checkForUpdates, "checkForUpdates");
319
- }
320
- __name(apply, "apply");
321
- var tempDir = import_path.default.join(process.cwd(), "tmp");
322
- async function downloadVideosToBase64(videoUrls, maxSize = 20) {
323
- await import_promises.default.mkdir(tempDir, { recursive: true });
324
- const results = [];
325
- for (const url of videoUrls) {
326
- let outputPath = null;
412
+ async function renderTweetScreenshot(tweetId) {
413
+ const puppeteer = ctx.puppeteer;
414
+ if (!puppeteer) {
415
+ throw new Error("Puppeteer 服务未找到,请安装 koishi-plugin-puppeteer");
416
+ }
417
+ const page = await puppeteer.page();
327
418
  try {
328
- outputPath = import_path.default.join(tempDir, `video_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.mp4`);
329
- await new Promise((resolve, reject) => {
330
- (0, import_fluent_ffmpeg.default)(url).inputOptions([
331
- "-protocol_whitelist",
332
- "file,http,https,tcp,tls,crypto"
333
- ]).outputOptions([
334
- "-c",
335
- "copy"
336
- ]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
337
- });
338
- const stats = await import_promises.default.stat(outputPath);
339
- const fileSizeInMB = stats.size / (1024 * 1024);
340
- if (fileSizeInMB > maxSize) {
341
- logger.info(`视频大小 ${fileSizeInMB.toFixed(2)}MB 超过${maxSize}MB限制,跳过转换`);
342
- } else {
343
- const fileBuffer = await import_promises.default.readFile(outputPath);
344
- const base64String = fileBuffer.toString("base64");
345
- const dataUrl = `data:video/mp4;base64,${base64String}`;
346
- results.push(dataUrl);
347
- }
348
- } catch (error) {
349
- console.error(`处理失败 (${url}):`, error.message);
350
- results.push(null);
351
- } finally {
352
- if (outputPath) {
419
+ await page.setCacheEnabled(false);
420
+ const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
421
+ await retry(3, async () => {
422
+ await page.goto(tweetUrl);
423
+ const element2 = await page.$(".main-thread");
424
+ if (!element2) throw new Error("Rate Limited");
425
+ }, 2e3);
426
+ if (config.enableTranslate) {
353
427
  try {
354
- await import_promises.default.unlink(outputPath);
355
- } catch (deleteError) {
428
+ await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
429
+ } catch (e) {
430
+ ctx.logger("nitter").info("翻译失败", e);
356
431
  }
357
432
  }
358
- }
359
- }
360
- return results;
361
- }
362
- __name(downloadVideosToBase64, "downloadVideosToBase64");
363
- async function renderTweetScreenshot(ctx, tweetId, config) {
364
- const puppeteer = ctx.puppeteer;
365
- if (!puppeteer) {
366
- throw new Error("Puppeteer 服务未找到,请安装 koishi-plugin-puppeteer");
367
- }
368
- const page = await puppeteer.page();
369
- try {
370
- const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
371
- await retry(3, async () => {
372
- await page.goto(tweetUrl);
373
- const element2 = await page.$(".main-thread");
374
- if (!element2) throw new Error("Rate Limited");
375
- }, 2e3);
376
- if (config.enableTranslate) {
377
- try {
378
- await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
379
- } catch (e) {
380
- ctx.logger("nitter").info("翻译失败", e);
381
- }
382
- }
383
- await new Promise((resolve) => setTimeout(resolve, 200));
384
- await page.evaluate(() => {
385
- const nav = document.querySelector("nav");
386
- if (nav) {
387
- nav.style.visibility = "hidden";
433
+ await page.evaluate(() => {
434
+ const nav = document.querySelector("nav");
435
+ if (nav) {
436
+ nav.style.visibility = "hidden";
437
+ }
438
+ });
439
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
440
+ await page.evaluate(() => {
441
+ const element2 = document.querySelector(".main-thread");
442
+ if (!element2) return;
443
+ Object.assign(element2.style, {
444
+ border: "1px solid #1DA1F2",
445
+ borderRadius: "8px",
446
+ boxShadow: "0px 1px 9px 12px rgba(29, 161, 242, 0.2)",
447
+ margin: "20px",
448
+ boxSizing: "border-box",
449
+ overflow: "hidden",
450
+ width: "100%",
451
+ padding: "10px 10px 0 10px",
452
+ backgroundColor: "#fff"
453
+ });
454
+ });
455
+ await new Promise((resolve) => setTimeout(resolve, 100));
456
+ const element = await page.$(".main-thread");
457
+ const boundingBox = await page.evaluate((el) => {
458
+ const rect = el.getBoundingClientRect();
459
+ const margin = 20;
460
+ return {
461
+ x: rect.left - margin,
462
+ y: rect.top - margin,
463
+ width: rect.width + margin * 2,
464
+ height: rect.height + margin * 2
465
+ };
466
+ }, element);
467
+ const buffer = await page.screenshot({
468
+ type: "png",
469
+ omitBackground: false,
470
+ clip: boundingBox
471
+ });
472
+ if (config.sendPic) {
473
+ const originalImages = await page.$$eval(
474
+ ".main-tweet a.still-image",
475
+ (links, baseUrl) => links.map((link) => link.getAttribute("href")).filter((href) => href).map((href) => `${baseUrl}${href}`),
476
+ config.nitterUrl
477
+ );
478
+ const hlsUrls = await page.$$eval(".main-tweet video", (videos, baseUrl) => {
479
+ return videos.map((video) => video.getAttribute("data-url")).filter((dataUrl) => dataUrl).map((dataUrl) => `${baseUrl}${dataUrl}`);
480
+ }, config.nitterUrl);
481
+ return [buffer, originalImages, hlsUrls];
388
482
  }
389
- });
390
- const element = await page.$(".main-thread");
391
- const buffer = await element.screenshot({
392
- // 不指定path参数,直接返回buffer
393
- type: "png",
394
- omitBackground: false
395
- });
396
- if (config.sendPic) {
397
- const originalImages = await page.$$eval(
398
- ".main-tweet a.still-image",
399
- (links, baseUrl) => links.map((link) => link.getAttribute("href")).filter((href) => href).map((href) => `${baseUrl}${href}`),
400
- config.nitterUrl
401
- );
402
- const hlsUrls = await page.$$eval(".main-tweet video", (videos, baseUrl) => {
403
- return videos.map((video) => video.getAttribute("data-url")).filter((dataUrl) => dataUrl).map((dataUrl) => `${baseUrl}${dataUrl}`);
404
- }, config.nitterUrl);
405
- return [buffer, originalImages, hlsUrls];
483
+ return [buffer, [], []];
484
+ } catch (e) {
485
+ throw e;
486
+ } finally {
487
+ await page.close();
406
488
  }
407
- return [buffer, [], []];
408
- } catch (e) {
409
- throw e;
410
- } finally {
411
- await page.close();
412
489
  }
490
+ __name(renderTweetScreenshot, "renderTweetScreenshot");
413
491
  }
414
- __name(renderTweetScreenshot, "renderTweetScreenshot");
492
+ __name(apply, "apply");
415
493
  // Annotate the CommonJS export names for ESM import in node:
416
494
  0 && (module.exports = {
417
495
  Config,
418
496
  apply,
419
- downloadVideosToBase64,
420
497
  inject,
421
498
  name
422
499
  });
package/lib/retry.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function retry<T>(retries: number, fn: () => Promise<T> | T, delay?: number): Promise<T>;
@@ -0,0 +1,8 @@
1
+ export declare class taskQueue<T> {
2
+ private queue;
3
+ private consumer?;
4
+ private processing;
5
+ push(task: T): void;
6
+ onProcess(handler: (task: T) => Promise<void> | void): void;
7
+ private process;
8
+ }
@@ -1,6 +1,4 @@
1
1
  import { Page } from 'puppeteer-core';
2
- export declare function retry(retries: number, fn: () => any, delay?: number): any;
3
2
  export declare function setGoogleTranslate(key: string, proxy: string): void;
4
- export declare function setSiliconTranslate(key: string, model: string, prompt: string, timeout?: number): void;
5
3
  export declare function setOpenAiTranslate(url: string, key: string, model: string, prompt: string, temperature: number, timeout?: number): void;
6
4
  export declare function addTranslate(page: Page, className: string): Promise<void>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-nitter",
3
3
  "description": "使用Rettiwt-API订阅推文,并使用nitter渲染",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -19,11 +19,11 @@
19
19
  "axios": "^1.12.2",
20
20
  "https-proxy-agent": "^7.0.6",
21
21
  "koishi": "^4.18.9",
22
- "koishi-plugin-subscription": "^0.0.5"
22
+ "koishi-plugin-subscription": "^0.0.6"
23
23
  },
24
24
  "dependencies": {
25
+ "fluent-ffmpeg": "^2.1.3",
25
26
  "node-cron": "^4.2.1",
26
- "rettiwt-api": "^6.0.6",
27
- "fluent-ffmpeg": "^2.1.3"
27
+ "rettiwt-api": "^6.1.7"
28
28
  }
29
29
  }