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.
- package/lib/download.d.ts +2 -0
- package/lib/fixedSet.d.ts +7 -0
- package/lib/index.d.ts +8 -1
- package/lib/index.js +180 -108
- package/lib/taskqueue.d.ts +8 -0
- package/lib/translate.d.ts +0 -1
- package/package.json +3 -3
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
|
|
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(
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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(
|
|
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
|
-
|
|
356
|
+
queue.push({ account, tweetId });
|
|
274
357
|
});
|
|
275
|
-
async function broadcast(
|
|
358
|
+
async function broadcast(task) {
|
|
359
|
+
const { account, tweetId } = task;
|
|
276
360
|
try {
|
|
277
|
-
const [screenshot, imageUrls, hlsUrls] = await renderTweetScreenshot(
|
|
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.
|
|
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
|
-
|
|
404
|
+
queue.push({ account: data.tweetBy.userName, tweetId: data.id });
|
|
316
405
|
}
|
|
317
406
|
}
|
|
318
407
|
__name(checkForUpdates, "checkForUpdates");
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
329
|
-
await
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
355
|
-
} catch (
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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(
|
|
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
|
});
|
package/lib/translate.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
27
|
-
"fluent-ffmpeg": "^2.1.3"
|
|
27
|
+
"rettiwt-api": "^6.1.3"
|
|
28
28
|
}
|
|
29
29
|
}
|