koishi-plugin-nitter 0.0.7 → 0.0.8

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
@@ -18,6 +18,13 @@ export interface Config {
18
18
  sendPic: boolean;
19
19
  maxSize: number;
20
20
  }
21
+ declare module 'koishi' {
22
+ interface Tables {
23
+ nitter_records: {
24
+ id: string;
25
+ createdAt: Date;
26
+ };
27
+ }
28
+ }
21
29
  export declare const Config: Schema<Config, Dict>;
22
30
  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
  });
@@ -109,6 +108,7 @@ json输出格式:["翻译结果1", "翻译结果2", ...]`
109
108
  });
110
109
  if (response.data && response.data.choices && response.data.choices[0]) {
111
110
  const content = response.data.choices[0].message.content;
111
+ console.log(content);
112
112
  data = JSON.parse(content);
113
113
  if (!Array.isArray(data)) {
114
114
  if (typeof data === "object" && Object.keys(data).length === 1) {
@@ -122,7 +122,7 @@ json输出格式:["翻译结果1", "翻译结果2", ...]`
122
122
  throw new Error("API返回数据格式异常");
123
123
  }
124
124
  });
125
- return data;
125
+ return data.map((content) => content.replace(/\\n/g, "\n"));
126
126
  }, "translate");
127
127
  }
128
128
  __name(setOpenAiTranslate, "setOpenAiTranslate");
@@ -154,13 +154,87 @@ __name(addTranslate, "addTranslate");
154
154
  // src/index.tsx
155
155
  var import_rettiwt_api = require("rettiwt-api");
156
156
  var import_node_cron = require("node-cron");
157
+
158
+ // src/download.ts
157
159
  var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
158
160
  var import_path = __toESM(require("path"));
159
161
  var import_promises = __toESM(require("fs/promises"));
162
+ var import_axios2 = __toESM(require("axios"));
163
+ var tempDir = import_path.default.join(process.cwd(), "tmp");
164
+ async function downloadVideosToBase64(videoUrls, maxSize = 20) {
165
+ await import_promises.default.mkdir(tempDir, { recursive: true });
166
+ const results = [];
167
+ for (const url of videoUrls) {
168
+ let outputPath = null;
169
+ try {
170
+ outputPath = import_path.default.join(tempDir, `video_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.mp4`);
171
+ await new Promise((resolve, reject) => {
172
+ (0, import_fluent_ffmpeg.default)(url).inputOptions([
173
+ "-protocol_whitelist",
174
+ "file,http,https,tcp,tls,crypto"
175
+ ]).outputOptions([
176
+ "-c",
177
+ "copy"
178
+ ]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
179
+ });
180
+ const stats = await import_promises.default.stat(outputPath);
181
+ const fileSizeInMB = stats.size / (1024 * 1024);
182
+ if (fileSizeInMB > maxSize) {
183
+ console.log(`视频大小 ${fileSizeInMB.toFixed(2)}MB 超过${maxSize}MB限制,跳过转换`);
184
+ } else {
185
+ const fileBuffer = await import_promises.default.readFile(outputPath);
186
+ const base64String = fileBuffer.toString("base64");
187
+ const dataUrl = `data:video/mp4;base64,${base64String}`;
188
+ results.push(dataUrl);
189
+ }
190
+ } catch (error) {
191
+ console.error(`处理失败 (${url}):`, error.message);
192
+ results.push(null);
193
+ } finally {
194
+ if (outputPath) {
195
+ try {
196
+ await import_promises.default.unlink(outputPath);
197
+ } catch (deleteError) {
198
+ }
199
+ }
200
+ }
201
+ }
202
+ return results;
203
+ }
204
+ __name(downloadVideosToBase64, "downloadVideosToBase64");
205
+
206
+ // src/taskqueue.ts
207
+ var taskQueue = class {
208
+ static {
209
+ __name(this, "taskQueue");
210
+ }
211
+ queue = [];
212
+ consumer;
213
+ processing = false;
214
+ push(task) {
215
+ this.queue.push(task);
216
+ if (!this.processing) {
217
+ this.process();
218
+ }
219
+ }
220
+ onProcess(handler) {
221
+ this.consumer = handler;
222
+ }
223
+ async process() {
224
+ if (this.processing || !this.consumer) return;
225
+ this.processing = true;
226
+ while (this.queue.length > 0) {
227
+ const task = this.queue.shift();
228
+ await this.consumer(task);
229
+ }
230
+ this.processing = false;
231
+ }
232
+ };
233
+
234
+ // src/index.tsx
160
235
  var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
161
236
  var name = "nitter";
162
- var logger = new import_koishi.Logger(name);
163
- var inject = ["puppeteer", "subscription"];
237
+ var inject = ["puppeteer", "subscription", "database"];
164
238
  var Config = import_koishi.Schema.intersect([
165
239
  import_koishi.Schema.object({
166
240
  apiKey: import_koishi.Schema.string().required().description("Twitter API Key"),
@@ -200,18 +274,25 @@ var Config = import_koishi.Schema.intersect([
200
274
  import_koishi.Schema.union([
201
275
  import_koishi.Schema.object({
202
276
  sendPic: import_koishi.Schema.const(true).required(),
203
- maxSize: import_koishi.Schema.number().default(10).description("发送视频的最大大小,单位为mb")
277
+ maxSize: import_koishi.Schema.number().default(20).description("发送视频的最大大小,单位为mb")
204
278
  })
205
279
  ])
206
280
  ]);
207
281
  function apply(ctx, config) {
282
+ ctx.model.extend("nitter_records", {
283
+ id: "string",
284
+ createdAt: "timestamp"
285
+ }, {
286
+ primary: "id",
287
+ autoInc: false
288
+ });
208
289
  config.nitterUrl = config.nitterUrl.replace(/\/+$/, "");
209
290
  const twitterClient = new import_rettiwt_api.Rettiwt({
210
291
  apiKey: config.apiKey,
211
292
  proxyUrl: config.proxy ? new URL(config.proxy) : void 0
212
293
  });
213
- let latestTweetId;
214
294
  let cronJob;
295
+ const queue = new taskQueue();
215
296
  (async () => {
216
297
  if (config.enableTranslate == "google") {
217
298
  setGoogleTranslate(config.googleApiKey, config.proxy);
@@ -219,12 +300,14 @@ function apply(ctx, config) {
219
300
  setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
220
301
  }
221
302
  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);
303
+ await ctx.database.upsert("nitter_records", tweetList.map((data) => {
304
+ return {
305
+ id: data.id,
306
+ createdAt: new Date(data.createdAt)
307
+ };
308
+ }));
309
+ queue.onProcess(broadcast);
310
+ cronJob = (0, import_node_cron.schedule)("15 */5 * * * *", checkForUpdates);
228
311
  ctx.on("dispose", () => {
229
312
  cronJob.stop();
230
313
  });
@@ -250,7 +333,7 @@ function apply(ctx, config) {
250
333
  return "请输入推文ID";
251
334
  }
252
335
  try {
253
- const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
336
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(tweetId);
254
337
  let msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
255
338
  let forwardMsg = [];
256
339
  const videoUrls = await downloadVideosToBase64(hlsUrls, config.maxSize);
@@ -270,11 +353,12 @@ function apply(ctx, config) {
270
353
  if (!tweetId) {
271
354
  return "请输入推文ID";
272
355
  }
273
- broadcast(account, tweetId);
356
+ queue.push({ account, tweetId });
274
357
  });
275
- async function broadcast(account, tweetId) {
358
+ async function broadcast(task) {
359
+ const { account, tweetId } = task;
276
360
  try {
277
- const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(ctx, tweetId, config);
361
+ const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(tweetId);
278
362
  const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
279
363
  ctx.subscription.broadcast(config.app, account, screenshotMsg);
280
364
  let forwardMsg = [];
@@ -306,117 +390,105 @@ function apply(ctx, config) {
306
390
  for (const data of tweetList) {
307
391
  if (!config.enableReTweet && data.retweetedTweet)
308
392
  continue;
309
- if (data.id <= latestTweetId)
393
+ if ((/* @__PURE__ */ new Date()).getTime() - new Date(data.createdAt).getTime() > 60 * 60 * 1e3)
310
394
  continue;
395
+ if ((await ctx.database.get("nitter_records", { id: data.id })).length > 0)
396
+ continue;
397
+ await ctx.database.upsert("nitter_records", [{
398
+ id: data.id,
399
+ createdAt: new Date(data.createdAt)
400
+ }]);
311
401
  if (!ctx.subscription.getAvailableAccounts(config.app).includes(data.tweetBy.userName))
312
402
  continue;
313
- latestTweetId = data.id;
314
403
  ctx.logger("nitter").info(`检测到推文id:${data.id},开始推送`);
315
- await broadcast(data.tweetBy.userName, data.id);
404
+ queue.push({ account: data.tweetBy.userName, tweetId: data.id });
316
405
  }
317
406
  }
318
407
  __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;
408
+ async function renderTweetScreenshot(tweetId) {
409
+ const puppeteer = ctx.puppeteer;
410
+ if (!puppeteer) {
411
+ throw new Error("Puppeteer 服务未找到,请安装 koishi-plugin-puppeteer");
412
+ }
413
+ const page = await puppeteer.page();
327
414
  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) {
415
+ const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
416
+ await retry(3, async () => {
417
+ await page.goto(tweetUrl);
418
+ const element2 = await page.$(".main-thread");
419
+ if (!element2) throw new Error("Rate Limited");
420
+ }, 2e3);
421
+ if (config.enableTranslate) {
353
422
  try {
354
- await import_promises.default.unlink(outputPath);
355
- } catch (deleteError) {
423
+ await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
424
+ } catch (e) {
425
+ ctx.logger("nitter").info("翻译失败", e);
356
426
  }
357
427
  }
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";
428
+ await page.evaluate(() => {
429
+ const nav = document.querySelector("nav");
430
+ if (nav) {
431
+ nav.style.visibility = "hidden";
432
+ }
433
+ });
434
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
435
+ await page.evaluate(() => {
436
+ const element2 = document.querySelector(".main-thread");
437
+ if (!element2) return;
438
+ Object.assign(element2.style, {
439
+ border: "1px solid #1DA1F2",
440
+ borderRadius: "8px",
441
+ boxShadow: "0px 1px 9px 12px rgba(29, 161, 242, 0.2)",
442
+ margin: "20px",
443
+ boxSizing: "border-box",
444
+ overflow: "hidden",
445
+ width: "100%",
446
+ padding: "10px 10px 0 10px",
447
+ backgroundColor: "#fff"
448
+ });
449
+ });
450
+ await new Promise((resolve) => setTimeout(resolve, 100));
451
+ const element = await page.$(".main-thread");
452
+ const boundingBox = await page.evaluate((el) => {
453
+ const rect = el.getBoundingClientRect();
454
+ const margin = 20;
455
+ return {
456
+ x: rect.left - margin,
457
+ y: rect.top - margin,
458
+ width: rect.width + margin * 2,
459
+ height: rect.height + margin * 2
460
+ };
461
+ }, element);
462
+ const buffer = await page.screenshot({
463
+ type: "png",
464
+ omitBackground: false,
465
+ clip: boundingBox
466
+ });
467
+ if (config.sendPic) {
468
+ const originalImages = await page.$$eval(
469
+ ".main-tweet a.still-image",
470
+ (links, baseUrl) => links.map((link) => link.getAttribute("href")).filter((href) => href).map((href) => `${baseUrl}${href}`),
471
+ config.nitterUrl
472
+ );
473
+ const hlsUrls = await page.$$eval(".main-tweet video", (videos, baseUrl) => {
474
+ return videos.map((video) => video.getAttribute("data-url")).filter((dataUrl) => dataUrl).map((dataUrl) => `${baseUrl}${dataUrl}`);
475
+ }, config.nitterUrl);
476
+ return [buffer, originalImages, hlsUrls];
388
477
  }
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];
478
+ return [buffer, [], []];
479
+ } catch (e) {
480
+ throw e;
481
+ } finally {
482
+ await page.close();
406
483
  }
407
- return [buffer, [], []];
408
- } catch (e) {
409
- throw e;
410
- } finally {
411
- await page.close();
412
484
  }
485
+ __name(renderTweetScreenshot, "renderTweetScreenshot");
413
486
  }
414
- __name(renderTweetScreenshot, "renderTweetScreenshot");
487
+ __name(apply, "apply");
415
488
  // Annotate the CommonJS export names for ESM import in node:
416
489
  0 && (module.exports = {
417
490
  Config,
418
491
  apply,
419
- downloadVideosToBase64,
420
492
  inject,
421
493
  name
422
494
  });
@@ -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,5 @@
1
1
  import { Page } from 'puppeteer-core';
2
2
  export declare function retry(retries: number, fn: () => any, delay?: number): any;
3
3
  export declare function setGoogleTranslate(key: string, proxy: string): void;
4
- export declare function setSiliconTranslate(key: string, model: string, prompt: string, timeout?: number): void;
5
4
  export declare function setOpenAiTranslate(url: string, key: string, model: string, prompt: string, temperature: number, timeout?: number): void;
6
5
  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.8",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -22,8 +22,8 @@
22
22
  "koishi-plugin-subscription": "^0.0.5"
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.3"
28
28
  }
29
29
  }