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