koishi-plugin-nitter 0.0.10 → 0.0.12
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 -1
- package/lib/index.d.ts +3 -9
- package/lib/index.js +183 -111
- package/lib/retry.d.ts +2 -1
- package/lib/translate.d.ts +1 -1
- package/package.json +3 -3
package/lib/download.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export declare function downloadImagesToBase64(imageUrls: any): Promise<any[]>;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function downloadVideosToTempFiles(videoUrls: string[], tempDir: string, maxSize?: number): Promise<string[]>;
|
|
3
|
+
export declare function cleanupTempFiles(files: Array<string | null>): Promise<void>;
|
package/lib/index.d.ts
CHANGED
|
@@ -14,18 +14,12 @@ export interface Config {
|
|
|
14
14
|
temperature: number;
|
|
15
15
|
timeout?: number;
|
|
16
16
|
app: string;
|
|
17
|
+
enablePollingOnStart: boolean;
|
|
17
18
|
enableReTweet: boolean;
|
|
18
19
|
sendPic: boolean;
|
|
19
20
|
cronString: string;
|
|
20
|
-
maxSize
|
|
21
|
-
|
|
22
|
-
declare module 'koishi' {
|
|
23
|
-
interface Tables {
|
|
24
|
-
nitter_records: {
|
|
25
|
-
id: string;
|
|
26
|
-
createdAt: Date;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
21
|
+
maxSize?: number;
|
|
22
|
+
tmpDir?: string;
|
|
29
23
|
}
|
|
30
24
|
export declare const Config: Schema<Config, Dict>;
|
|
31
25
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -43,14 +43,19 @@ var import_axios = __toESM(require("axios"));
|
|
|
43
43
|
var import_https_proxy_agent = require("https-proxy-agent");
|
|
44
44
|
|
|
45
45
|
// src/retry.ts
|
|
46
|
-
async function retry(retries, fn,
|
|
46
|
+
async function retry(retries, fn, error_fn = (err) => {
|
|
47
|
+
throw err;
|
|
48
|
+
}, delay = 500) {
|
|
47
49
|
try {
|
|
48
50
|
return await fn();
|
|
49
51
|
} catch (err) {
|
|
50
52
|
console.log(`剩余${retries}次尝试`, err);
|
|
51
|
-
if (retries <= 1)
|
|
53
|
+
if (retries <= 1) {
|
|
54
|
+
error_fn(err);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
52
57
|
await new Promise((r) => setTimeout(r, delay));
|
|
53
|
-
return retry(retries - 1, fn, delay * 2);
|
|
58
|
+
return retry(retries - 1, fn, error_fn, delay * 2);
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
__name(retry, "retry");
|
|
@@ -137,6 +142,13 @@ async function addTranslate(page, className) {
|
|
|
137
142
|
}, className);
|
|
138
143
|
if (!elementsHTML || !elementsHTML.length) return;
|
|
139
144
|
const translatedHTMLs = await translate(elementsHTML);
|
|
145
|
+
const translatedTexts = await page.evaluate((htmlArr) => {
|
|
146
|
+
return htmlArr.map((html) => {
|
|
147
|
+
const div = document.createElement("div");
|
|
148
|
+
div.innerHTML = html;
|
|
149
|
+
return (div.innerText || "").replace(/\n{3,}/g, "\n\n").trim();
|
|
150
|
+
});
|
|
151
|
+
}, translatedHTMLs);
|
|
140
152
|
await page.evaluate((className2, originalHTMLs, translatedHTMLs2) => {
|
|
141
153
|
const elements = document.querySelectorAll(className2);
|
|
142
154
|
elements.forEach((element, index) => {
|
|
@@ -151,6 +163,7 @@ async function addTranslate(page, className) {
|
|
|
151
163
|
}
|
|
152
164
|
});
|
|
153
165
|
}, className, elementsHTML, translatedHTMLs);
|
|
166
|
+
return translatedTexts;
|
|
154
167
|
}
|
|
155
168
|
__name(addTranslate, "addTranslate");
|
|
156
169
|
|
|
@@ -163,48 +176,45 @@ var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
|
|
|
163
176
|
var import_path = __toESM(require("path"));
|
|
164
177
|
var import_promises = __toESM(require("fs/promises"));
|
|
165
178
|
var import_axios2 = __toESM(require("axios"));
|
|
166
|
-
|
|
167
|
-
async function downloadVideosToBase64(videoUrls, maxSize = 20) {
|
|
179
|
+
async function downloadVideosToTempFiles(videoUrls, tempDir, maxSize = 20) {
|
|
168
180
|
await import_promises.default.mkdir(tempDir, { recursive: true });
|
|
169
181
|
const results = [];
|
|
170
182
|
for (const url of videoUrls) {
|
|
171
183
|
let outputPath = null;
|
|
172
184
|
try {
|
|
173
|
-
outputPath = import_path.default.join(
|
|
185
|
+
outputPath = import_path.default.join(
|
|
186
|
+
tempDir,
|
|
187
|
+
`video_${Date.now()}_${crypto.randomUUID()}.mp4`
|
|
188
|
+
);
|
|
174
189
|
await new Promise((resolve, reject) => {
|
|
175
|
-
(0, import_fluent_ffmpeg.default)(url).inputOptions([
|
|
176
|
-
"-protocol_whitelist",
|
|
177
|
-
"file,http,https,tcp,tls,crypto"
|
|
178
|
-
]).outputOptions([
|
|
179
|
-
"-c",
|
|
180
|
-
"copy"
|
|
181
|
-
]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
|
|
190
|
+
(0, import_fluent_ffmpeg.default)(url).inputOptions(["-protocol_whitelist", "file,http,https,tcp,tls,crypto"]).outputOptions(["-c", "copy"]).output(outputPath).on("end", () => resolve(outputPath)).on("error", reject).run();
|
|
182
191
|
});
|
|
183
192
|
const stats = await import_promises.default.stat(outputPath);
|
|
184
193
|
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
185
194
|
if (fileSizeInMB > maxSize) {
|
|
186
|
-
console.log(`视频大小 ${fileSizeInMB.toFixed(2)}MB
|
|
195
|
+
console.log(`视频大小 ${fileSizeInMB.toFixed(2)}MB 超过 ${maxSize}MB,跳过发送`);
|
|
196
|
+
await import_promises.default.unlink(outputPath).catch(() => {
|
|
197
|
+
});
|
|
198
|
+
results.push(null);
|
|
187
199
|
} else {
|
|
188
|
-
|
|
189
|
-
const base64String = fileBuffer.toString("base64");
|
|
190
|
-
const dataUrl = `data:video/mp4;base64,${base64String}`;
|
|
191
|
-
results.push(dataUrl);
|
|
200
|
+
results.push(outputPath);
|
|
192
201
|
}
|
|
193
202
|
} catch (error) {
|
|
194
|
-
console.error(
|
|
203
|
+
console.error(`下载失败 (${url}):`, error?.message || error);
|
|
204
|
+
if (outputPath) await import_promises.default.unlink(outputPath).catch(() => {
|
|
205
|
+
});
|
|
195
206
|
results.push(null);
|
|
196
|
-
} finally {
|
|
197
|
-
if (outputPath) {
|
|
198
|
-
try {
|
|
199
|
-
await import_promises.default.unlink(outputPath);
|
|
200
|
-
} catch (deleteError) {
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
return results;
|
|
206
210
|
}
|
|
207
|
-
__name(
|
|
211
|
+
__name(downloadVideosToTempFiles, "downloadVideosToTempFiles");
|
|
212
|
+
async function cleanupTempFiles(files) {
|
|
213
|
+
await Promise.allSettled(
|
|
214
|
+
files.filter((f) => !!f).map((f) => import_promises.default.unlink(f))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
__name(cleanupTempFiles, "cleanupTempFiles");
|
|
208
218
|
|
|
209
219
|
// src/taskqueue.ts
|
|
210
220
|
var taskQueue = class {
|
|
@@ -237,7 +247,7 @@ var taskQueue = class {
|
|
|
237
247
|
// src/index.tsx
|
|
238
248
|
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
239
249
|
var name = "nitter";
|
|
240
|
-
var inject = ["puppeteer", "subscription"
|
|
250
|
+
var inject = ["puppeteer", "subscription"];
|
|
241
251
|
var Config = import_koishi.Schema.intersect([
|
|
242
252
|
import_koishi.Schema.object({
|
|
243
253
|
apiKey: import_koishi.Schema.string().required().description("Twitter API Key"),
|
|
@@ -271,6 +281,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
271
281
|
app: import_koishi.Schema.string().description("subscription配置中应用名")
|
|
272
282
|
}).description("订阅配置"),
|
|
273
283
|
import_koishi.Schema.object({
|
|
284
|
+
enablePollingOnStart: import_koishi.Schema.boolean().default(true).description("启动后是否立刻开始轮询"),
|
|
274
285
|
enableReTweet: import_koishi.Schema.boolean().default(false).description("是否发送转推"),
|
|
275
286
|
sendPic: import_koishi.Schema.boolean().default(false).description("是否单独发送推文中的图片"),
|
|
276
287
|
cronString: import_koishi.Schema.string().default("15 */5 * * * *").description("使用cron表达式描述检查更新的时间,默认为每隔5分钟检查一次")
|
|
@@ -278,18 +289,12 @@ var Config = import_koishi.Schema.intersect([
|
|
|
278
289
|
import_koishi.Schema.union([
|
|
279
290
|
import_koishi.Schema.object({
|
|
280
291
|
sendPic: import_koishi.Schema.const(true).required(),
|
|
281
|
-
maxSize: import_koishi.Schema.number().default(20).description("发送视频的最大大小,单位为mb")
|
|
292
|
+
maxSize: import_koishi.Schema.number().default(20).description("发送视频的最大大小,单位为mb"),
|
|
293
|
+
tmpDir: import_koishi.Schema.string().default("/shared/tmp").description("临时存放视频的目录,需要koishi和对接的应用都能访问")
|
|
282
294
|
})
|
|
283
295
|
])
|
|
284
296
|
]);
|
|
285
297
|
function apply(ctx, config) {
|
|
286
|
-
ctx.model.extend("nitter_records", {
|
|
287
|
-
id: "string",
|
|
288
|
-
createdAt: "timestamp"
|
|
289
|
-
}, {
|
|
290
|
-
primary: "id",
|
|
291
|
-
autoInc: false
|
|
292
|
-
});
|
|
293
298
|
config.nitterUrl = config.nitterUrl.replace(/\/+$/, "");
|
|
294
299
|
const twitterClient = new import_rettiwt_api.Rettiwt({
|
|
295
300
|
apiKey: config.apiKey,
|
|
@@ -304,19 +309,30 @@ function apply(ctx, config) {
|
|
|
304
309
|
} else if (config.enableTranslate == "openai") {
|
|
305
310
|
setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
|
|
306
311
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
312
|
+
queue.onProcess(
|
|
313
|
+
(task) => {
|
|
314
|
+
return work(
|
|
315
|
+
task.tweetId,
|
|
316
|
+
async (msg) => await ctx.subscription.broadcast(config.app, task.account, msg),
|
|
317
|
+
async (forwardMsg) => await ctx.subscription.broadcastForward(config.app, task.account, forwardMsg)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
if (config.enablePollingOnStart) {
|
|
322
|
+
const tweetList = await getFollowedFeed();
|
|
323
|
+
if (tweetList.length === 0) {
|
|
324
|
+
ctx.logger("nitter").info("初始化失败,请重试");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
await Promise.all(tweetList.map((data) => {
|
|
328
|
+
return ctx.subscription.checkExist(config.app, data.id);
|
|
329
|
+
}));
|
|
330
|
+
cronJob = (0, import_node_cron.schedule)(config.cronString, checkForUpdates);
|
|
331
|
+
ctx.on("dispose", () => {
|
|
332
|
+
cronJob.stop();
|
|
333
|
+
});
|
|
334
|
+
ctx.logger("nitter").info("开始监听推特动态");
|
|
335
|
+
}
|
|
320
336
|
})();
|
|
321
337
|
ctx.command("nitter.follow", "按照subscription中的订阅配置,使用登录的推特账号关注所有需要订阅的账号", { authority: 3 }).action(async () => {
|
|
322
338
|
const whiteList = ctx.subscription.getAvailableAccounts(config.app);
|
|
@@ -327,12 +343,13 @@ function apply(ctx, config) {
|
|
|
327
343
|
let user;
|
|
328
344
|
await retry(3, async () => {
|
|
329
345
|
user = await twitterClient.user.details(id);
|
|
330
|
-
});
|
|
346
|
+
}, (err) => ctx.logger("nitter").error(`获取用户${id}信息失败: ${err}`));
|
|
347
|
+
if (!user) continue;
|
|
331
348
|
await new Promise((resolve) => setTimeout(resolve, 3 * 1e3));
|
|
332
349
|
await retry(3, async () => {
|
|
333
350
|
await twitterClient.user.follow(user.id);
|
|
334
|
-
|
|
335
|
-
|
|
351
|
+
ctx.logger("nitter").info(`关注${id}成功`);
|
|
352
|
+
}, (err) => ctx.logger("nitter").error(`关注${id}失败: ${err}`));
|
|
336
353
|
await new Promise((resolve) => setTimeout(resolve, 30 * 1e3));
|
|
337
354
|
}
|
|
338
355
|
}
|
|
@@ -343,17 +360,11 @@ function apply(ctx, config) {
|
|
|
343
360
|
return "请输入推文ID";
|
|
344
361
|
}
|
|
345
362
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
|
|
352
|
-
}
|
|
353
|
-
if (videoUrls.length > 0) {
|
|
354
|
-
forwardMsg.push(...videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: url }) })));
|
|
355
|
-
}
|
|
356
|
-
await session.send(msg + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: forwardMsg }));
|
|
363
|
+
await work(
|
|
364
|
+
tweetId,
|
|
365
|
+
async (msg) => await session.send(msg),
|
|
366
|
+
async (forwardMsg) => await session.send(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: forwardMsg }))
|
|
367
|
+
);
|
|
357
368
|
} catch (error) {
|
|
358
369
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
359
370
|
return "获取推文失败";
|
|
@@ -365,57 +376,82 @@ function apply(ctx, config) {
|
|
|
365
376
|
}
|
|
366
377
|
queue.push({ account, tweetId });
|
|
367
378
|
});
|
|
368
|
-
async function
|
|
369
|
-
|
|
379
|
+
async function work(tweetId, sendFunc, sendForwardFunc) {
|
|
380
|
+
let videoFiles = [];
|
|
370
381
|
try {
|
|
371
|
-
const
|
|
382
|
+
const { screenshot, pieces } = await renderTweetScreenshot(tweetId);
|
|
372
383
|
const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const videoUrls = await downloadVideosToBase64(hlsUrls, config.maxSize);
|
|
380
|
-
if (imageUrls.length > 0) {
|
|
381
|
-
forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
|
|
384
|
+
await sendFunc(screenshotMsg);
|
|
385
|
+
const forwardMsg = [];
|
|
386
|
+
const videoUrls = pieces.filter((p) => p.type === "video").map((p) => p.url);
|
|
387
|
+
const tmpDir = config.tmpDir ?? "/shared/tmp";
|
|
388
|
+
if (videoUrls.length) {
|
|
389
|
+
videoFiles = await downloadVideosToTempFiles(videoUrls, tmpDir, config.maxSize ?? 20);
|
|
382
390
|
}
|
|
383
|
-
|
|
384
|
-
|
|
391
|
+
let vIdx = 0;
|
|
392
|
+
for (const p of pieces) {
|
|
393
|
+
if (p.type === "account") {
|
|
394
|
+
forwardMsg.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: p.text }));
|
|
395
|
+
} else if (p.type === "text") {
|
|
396
|
+
forwardMsg.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: p.text }));
|
|
397
|
+
} 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
|
+
);
|
|
401
|
+
} else if (p.type === "video") {
|
|
402
|
+
const file = videoFiles[vIdx++];
|
|
403
|
+
if (file) {
|
|
404
|
+
forwardMsg.push(
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: "file://" + file }) })
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
385
409
|
}
|
|
386
410
|
if (forwardMsg.length > 0) {
|
|
387
|
-
await
|
|
411
|
+
await sendForwardFunc(forwardMsg);
|
|
388
412
|
}
|
|
389
413
|
} catch (error) {
|
|
390
414
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
415
|
+
} finally {
|
|
416
|
+
await cleanupTempFiles(videoFiles);
|
|
391
417
|
}
|
|
392
418
|
}
|
|
393
|
-
__name(
|
|
419
|
+
__name(work, "work");
|
|
394
420
|
async function getFollowedFeed() {
|
|
395
421
|
let tweetList;
|
|
396
|
-
await retry(
|
|
422
|
+
await retry(5, async () => {
|
|
397
423
|
({ list: tweetList } = await twitterClient.user.followed());
|
|
398
|
-
});
|
|
424
|
+
}, (err) => ctx.logger("nitter").error(`获取推文列表失败: ${err}`));
|
|
425
|
+
if (!tweetList) return [];
|
|
399
426
|
return tweetList.reverse();
|
|
400
427
|
}
|
|
401
428
|
__name(getFollowedFeed, "getFollowedFeed");
|
|
429
|
+
let checking;
|
|
402
430
|
async function checkForUpdates() {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
431
|
+
if (checking) {
|
|
432
|
+
ctx.logger("nitter").info(`${new Date(checking).toLocaleTimeString()}开始的检查尚未完成`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
checking = Date.now();
|
|
436
|
+
try {
|
|
437
|
+
const tweetList = await getFollowedFeed();
|
|
438
|
+
if (tweetList.length === 0) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const data of tweetList) {
|
|
442
|
+
if (!config.enableReTweet && data.retweetedTweet)
|
|
443
|
+
continue;
|
|
444
|
+
if ((/* @__PURE__ */ new Date()).getTime() - new Date(data.createdAt).getTime() > 2 * 60 * 60 * 1e3)
|
|
445
|
+
continue;
|
|
446
|
+
if (await ctx.subscription.checkExist(config.app, data.id))
|
|
447
|
+
continue;
|
|
448
|
+
if (!ctx.subscription.getAvailableAccounts(config.app).includes(data.tweetBy.userName))
|
|
449
|
+
continue;
|
|
450
|
+
ctx.logger("nitter").info(`检测到推文id:${data.id},开始推送`);
|
|
451
|
+
queue.push({ account: data.tweetBy.userName, tweetId: data.id });
|
|
452
|
+
}
|
|
453
|
+
} finally {
|
|
454
|
+
checking = void 0;
|
|
419
455
|
}
|
|
420
456
|
}
|
|
421
457
|
__name(checkForUpdates, "checkForUpdates");
|
|
@@ -429,13 +465,48 @@ function apply(ctx, config) {
|
|
|
429
465
|
await page.setCacheEnabled(false);
|
|
430
466
|
const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
|
|
431
467
|
await retry(3, async () => {
|
|
432
|
-
await page.goto(tweetUrl);
|
|
468
|
+
await page.goto(tweetUrl, { timeout: 12e4 });
|
|
433
469
|
const element2 = await page.$(".main-thread");
|
|
434
470
|
if (!element2) throw new Error("Rate Limited");
|
|
471
|
+
}, (err) => {
|
|
472
|
+
throw new Error(`获取推文${tweetId}失败: ${err}`);
|
|
435
473
|
}, 2e3);
|
|
436
|
-
|
|
474
|
+
const captured = await page.evaluate((baseUrl) => {
|
|
475
|
+
const root = document.querySelector(".main-thread");
|
|
476
|
+
if (!root) return { accountText: "unknown", flow: [] };
|
|
477
|
+
const fullName = (root.querySelector(".fullname")?.textContent || "").trim();
|
|
478
|
+
const username = (root.querySelector(".username")?.textContent || "").trim();
|
|
479
|
+
const accountText = fullName && username ? `${fullName} ${username}` : username || fullName || "unknown";
|
|
480
|
+
const selector = ".tweet-content, .quote-text, a.still-image[href], video[data-url]";
|
|
481
|
+
const nodes = Array.from(root.querySelectorAll(selector));
|
|
482
|
+
const flow = [];
|
|
483
|
+
let textIndex = 0;
|
|
484
|
+
for (const n of nodes) {
|
|
485
|
+
const el = n;
|
|
486
|
+
if (el.classList.contains("tweet-content") || el.classList.contains("quote-text")) {
|
|
487
|
+
const text = (el.innerText || "").replace(/\n{3,}/g, "\n\n").trim();
|
|
488
|
+
flow.push({ type: "text", textIndex, fallbackText: text });
|
|
489
|
+
textIndex++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (el.tagName.toLowerCase() === "a" && el.classList.contains("still-image")) {
|
|
493
|
+
const href = el.getAttribute("href");
|
|
494
|
+
if (href) flow.push({ type: "image", url: `${baseUrl}${href}` });
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (el.tagName.toLowerCase() === "video") {
|
|
498
|
+
const dataUrl = el.getAttribute("data-url");
|
|
499
|
+
if (dataUrl) flow.push({ type: "video", url: `${baseUrl}${dataUrl}` });
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { accountText, flow };
|
|
504
|
+
}, config.nitterUrl);
|
|
505
|
+
let translatedTexts = [];
|
|
506
|
+
if (config.enableTranslate != "disable") {
|
|
437
507
|
try {
|
|
438
|
-
await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
|
|
508
|
+
const result = await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
|
|
509
|
+
if (result?.length) translatedTexts = result;
|
|
439
510
|
} catch (e) {
|
|
440
511
|
ctx.logger("nitter").info("翻译失败", e);
|
|
441
512
|
}
|
|
@@ -479,18 +550,19 @@ function apply(ctx, config) {
|
|
|
479
550
|
omitBackground: false,
|
|
480
551
|
clip: boundingBox
|
|
481
552
|
});
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
553
|
+
const pieces = [{ type: "account", text: `${captured.accountText}
|
|
554
|
+
https://x.com/i/status/${tweetId}` }];
|
|
555
|
+
for (const item of captured.flow) {
|
|
556
|
+
if (item.type === "text") {
|
|
557
|
+
const t = translatedTexts[item.textIndex] || item.fallbackText || "";
|
|
558
|
+
if (t.trim()) pieces.push({ type: "text", text: t });
|
|
559
|
+
} else if (item.type === "image" && config.sendPic) {
|
|
560
|
+
pieces.push({ type: "image", url: item.url });
|
|
561
|
+
} else if (item.type === "video" && config.sendPic) {
|
|
562
|
+
pieces.push({ type: "video", url: item.url });
|
|
563
|
+
}
|
|
492
564
|
}
|
|
493
|
-
return
|
|
565
|
+
return { screenshot: buffer, pieces };
|
|
494
566
|
} catch (e) {
|
|
495
567
|
throw e;
|
|
496
568
|
} finally {
|
package/lib/retry.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function retry<T>(retries: number, fn: () => Promise<T> | T, delay?: number): Promise<T>;
|
|
1
|
+
export declare function retry<T>(retries: number, fn: () => Promise<T> | T, error_fn?: (err: any) => void, delay?: number): Promise<T>;
|
|
2
|
+
export declare function withTimeout(promise: any, ms: any, fallbackValue?: any): Promise<any>;
|
package/lib/translate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Page } from 'puppeteer-core';
|
|
2
2
|
export declare function setGoogleTranslate(key: string, proxy: string): void;
|
|
3
3
|
export declare function setOpenAiTranslate(url: string, key: string, model: string, prompt: string, temperature: number, timeout?: number): void;
|
|
4
|
-
export declare function addTranslate(page: Page, className: string): Promise<
|
|
4
|
+
export declare function addTranslate(page: Page, className: string): Promise<any>;
|
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.12",
|
|
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.
|
|
22
|
+
"koishi-plugin-subscription": "^0.0.7"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"fluent-ffmpeg": "^2.1.3",
|
|
26
26
|
"node-cron": "^4.2.1",
|
|
27
|
-
"rettiwt-api": "^6.1
|
|
27
|
+
"rettiwt-api": "^6.2.1"
|
|
28
28
|
}
|
|
29
29
|
}
|