koishi-plugin-nitter 0.0.16 → 0.0.18

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/download.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare function downloadImagesToBase64(imageUrls: any): Promise<any[]>;
1
+ export declare function downloadImagesToBase64(imageUrls: string[]): Promise<Array<string | null>>;
2
2
  export declare function downloadVideosToTempFiles(videoUrls: string[], tempDir: string, maxSize?: number): Promise<string[]>;
3
3
  export declare function cleanupTempFiles(files: Array<string | null>): Promise<void>;
package/lib/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface Config {
14
14
  temperature: number;
15
15
  timeout?: number;
16
16
  app: string;
17
+ ownerAccount?: string;
17
18
  enablePollingOnStart: boolean;
18
19
  enableReTweet: boolean;
19
20
  sendPic: boolean;
package/lib/index.js CHANGED
@@ -176,6 +176,19 @@ var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
176
176
  var import_path = __toESM(require("path"));
177
177
  var import_promises = __toESM(require("fs/promises"));
178
178
  var import_axios2 = __toESM(require("axios"));
179
+ async function downloadImagesToBase64(imageUrls) {
180
+ return Promise.all(imageUrls.map(async (url) => {
181
+ try {
182
+ const response = await import_axios2.default.get(url, { responseType: "arraybuffer" });
183
+ const base64 = Buffer.from(response.data).toString("base64");
184
+ const contentType = response.headers?.["content-type"] || "image/jpeg";
185
+ return `data:${contentType};base64,${base64}`;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }));
190
+ }
191
+ __name(downloadImagesToBase64, "downloadImagesToBase64");
179
192
  async function downloadVideosToTempFiles(videoUrls, tempDir, maxSize = 20) {
180
193
  await import_promises.default.mkdir(tempDir, { recursive: true });
181
194
  const results = [];
@@ -278,7 +291,8 @@ var Config = import_koishi.Schema.intersect([
278
291
  import_koishi.Schema.object({})
279
292
  ]),
280
293
  import_koishi.Schema.object({
281
- app: import_koishi.Schema.string().description("subscription配置中应用名")
294
+ app: import_koishi.Schema.string().description("subscription配置中应用名"),
295
+ ownerAccount: import_koishi.Schema.string().description("主人账号。轮询连续失败3次后会停止轮询并向该账号发送告警。可填写 userId,或 platform:userId 指定平台。")
282
296
  }).description("订阅配置"),
283
297
  import_koishi.Schema.object({
284
298
  enablePollingOnStart: import_koishi.Schema.boolean().default(true).description("启动后是否立刻开始轮询"),
@@ -302,6 +316,7 @@ function apply(ctx, config) {
302
316
  logging: true
303
317
  });
304
318
  let cronJob;
319
+ let pollingFailures = 0;
305
320
  const queue = new taskQueue();
306
321
  (async () => {
307
322
  if (config.enableTranslate == "google") {
@@ -336,7 +351,21 @@ function apply(ctx, config) {
336
351
  })();
337
352
  ctx.command("nitter.follow", "按照subscription中的订阅配置,使用登录的推特账号关注所有需要订阅的账号", { authority: 3 }).action(async () => {
338
353
  const whiteList = ctx.subscription.getAvailableAccounts(config.app);
339
- const { list: followingList } = await twitterClient.user.following();
354
+ const followingList = [];
355
+ let cursor = void 0;
356
+ while (true) {
357
+ const res = await retry(
358
+ 3,
359
+ async () => {
360
+ return await twitterClient.user.following(void 0, 100, cursor);
361
+ },
362
+ (err) => ctx.logger("nitter").error(`获取已关注账号列表失败: ${err}`)
363
+ );
364
+ followingList.push(...res.list);
365
+ if (!res.next || res.next.split("|")[0] == "0") break;
366
+ cursor = res.next;
367
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
368
+ }
340
369
  const followingIdList = followingList.map((user) => user.userName);
341
370
  for (const id of whiteList) {
342
371
  if (!followingIdList.includes(id)) {
@@ -350,7 +379,7 @@ function apply(ctx, config) {
350
379
  await twitterClient.user.follow(user.id);
351
380
  ctx.logger("nitter").info(`关注${id}成功`);
352
381
  }, (err) => ctx.logger("nitter").error(`关注${id}失败: ${err}`));
353
- await new Promise((resolve) => setTimeout(resolve, 30 * 1e3));
382
+ await new Promise((resolve) => setTimeout(resolve, 60 * 1e3));
354
383
  }
355
384
  }
356
385
  return "关注完成";
@@ -384,10 +413,13 @@ function apply(ctx, config) {
384
413
  await sendFunc(screenshotMsg);
385
414
  const forwardMsg = [];
386
415
  const videoUrls = pieces.filter((p) => p.type === "video").map((p) => p.url);
416
+ const imageUrls = pieces.filter((p) => p.type === "image").map((p) => p.url);
387
417
  const tmpDir = config.tmpDir ?? "/shared/tmp";
418
+ const imageSources = imageUrls.length ? await downloadImagesToBase64(imageUrls) : [];
388
419
  if (videoUrls.length) {
389
420
  videoFiles = await downloadVideosToTempFiles(videoUrls, tmpDir, config.maxSize ?? 20);
390
421
  }
422
+ let iIdx = 0;
391
423
  let vIdx = 0;
392
424
  for (const p of pieces) {
393
425
  if (p.type === "account") {
@@ -395,9 +427,12 @@ function apply(ctx, config) {
395
427
  } else if (p.type === "text") {
396
428
  forwardMsg.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: p.text }));
397
429
  } else if (p.type === "image") {
398
- forwardMsg.push(
399
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: p.url }) })
400
- );
430
+ const src = imageSources[iIdx++];
431
+ if (src) {
432
+ forwardMsg.push(
433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src }) })
434
+ );
435
+ }
401
436
  } else if (p.type === "video") {
402
437
  const file = videoFiles[vIdx++];
403
438
  if (file) {
@@ -436,6 +471,7 @@ function apply(ctx, config) {
436
471
  checking = Date.now();
437
472
  try {
438
473
  const tweetList = await getFollowedFeed();
474
+ pollingFailures = 0;
439
475
  if (tweetList.length === 0) {
440
476
  return;
441
477
  }
@@ -451,11 +487,67 @@ function apply(ctx, config) {
451
487
  ctx.logger("nitter").info(`检测到推文id:${data.id},开始推送`);
452
488
  queue.push({ account: data.tweetBy.userName, tweetId: data.id });
453
489
  }
490
+ } catch (error) {
491
+ pollingFailures += 1;
492
+ ctx.logger("nitter").warn("检查推文更新失败:%o", error);
493
+ if (pollingFailures >= 3) {
494
+ cronJob?.stop();
495
+ const message = `Nitter 轮询连续失败 ${pollingFailures} 次,已停止轮询。
496
+ ${formatError(error)}`;
497
+ ctx.logger("nitter").warn(message);
498
+ await notifyOwner(message);
499
+ }
454
500
  } finally {
455
501
  checking = void 0;
456
502
  }
457
503
  }
458
504
  __name(checkForUpdates, "checkForUpdates");
505
+ async function notifyOwner(message) {
506
+ const owner = parseOwnerAccount(config.ownerAccount);
507
+ if (!owner) return;
508
+ try {
509
+ const bot = await findOwnerBot(owner.platform);
510
+ if (!bot) {
511
+ ctx.logger("nitter").warn("未找到可用于发送 Nitter 轮询告警的 bot");
512
+ return;
513
+ }
514
+ await bot.sendPrivateMessage(owner.userId, /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: message }));
515
+ } catch (error) {
516
+ ctx.logger("nitter").warn("发送 Nitter 轮询告警失败:%o", error);
517
+ }
518
+ }
519
+ __name(notifyOwner, "notifyOwner");
520
+ async function findOwnerBot(platform) {
521
+ if (platform) return ctx.bots.find((bot) => bot.platform === platform);
522
+ const accounts = ctx.subscription.getAvailableAccounts(config.app);
523
+ for (const account of accounts) {
524
+ const groups = await ctx.subscription.getSubscribedGroups(config.app, account);
525
+ for (const group of groups) {
526
+ const groupPlatform = group.split(":")[0];
527
+ const bot = ctx.bots.find((bot2) => bot2.platform === groupPlatform);
528
+ if (bot) return bot;
529
+ }
530
+ }
531
+ return ctx.bots[0];
532
+ }
533
+ __name(findOwnerBot, "findOwnerBot");
534
+ function parseOwnerAccount(input) {
535
+ const value = input?.trim();
536
+ if (!value) return null;
537
+ const index = value.indexOf(":");
538
+ if (index > 0) {
539
+ return {
540
+ platform: value.slice(0, index),
541
+ userId: value.slice(index + 1)
542
+ };
543
+ }
544
+ return { userId: value };
545
+ }
546
+ __name(parseOwnerAccount, "parseOwnerAccount");
547
+ function formatError(error) {
548
+ return error?.message || String(error);
549
+ }
550
+ __name(formatError, "formatError");
459
551
  async function renderTweetScreenshot(tweetId) {
460
552
  const puppeteer = ctx.puppeteer;
461
553
  if (!puppeteer) {
@@ -523,15 +615,15 @@ function apply(ctx, config) {
523
615
  const element2 = document.querySelector(".main-thread");
524
616
  if (!element2) return;
525
617
  Object.assign(element2.style, {
526
- border: "1px solid #1DA1F2",
618
+ border: "3px solid transparent",
527
619
  borderRadius: "8px",
528
- boxShadow: "0px 1px 9px 12px rgba(29, 161, 242, 0.2)",
620
+ background: "linear-gradient(#fff, #fff) padding-box, linear-gradient(135deg, #1DA1F2 0%, #35C2FF 35%, #6EE7F9 68%, #A7F3D0 100%) border-box",
621
+ boxShadow: "0 8px 24px rgba(29, 161, 242, 0.18)",
529
622
  margin: "20px",
530
623
  boxSizing: "border-box",
531
624
  overflow: "hidden",
532
625
  width: "100%",
533
- padding: "20px 20px 10px 20px",
534
- backgroundColor: "#fff"
626
+ padding: "20px 20px 10px 20px"
535
627
  });
536
628
  });
537
629
  await new Promise((resolve) => setTimeout(resolve, 100));
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.16",
4
+ "version": "0.0.18",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -1,5 +1,5 @@
1
- # koishi-plugin-twitter
1
+ # koishi-plugin-nitter
2
2
 
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-twitter?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-twitter)
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-nitter?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-twitter)
4
4
 
5
5
  推文订阅