koishi-plugin-nitter 0.0.9 → 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 +178 -102
- 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,59 +160,58 @@ 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
|
|
|
157
167
|
// src/index.tsx
|
|
158
168
|
var import_rettiwt_api = require("rettiwt-api");
|
|
169
|
+
var import_node_cron = require("node-cron");
|
|
159
170
|
|
|
160
171
|
// src/download.ts
|
|
161
172
|
var import_fluent_ffmpeg = __toESM(require("fluent-ffmpeg"));
|
|
162
173
|
var import_path = __toESM(require("path"));
|
|
163
174
|
var import_promises = __toESM(require("fs/promises"));
|
|
164
175
|
var import_axios2 = __toESM(require("axios"));
|
|
165
|
-
|
|
166
|
-
async function downloadVideosToBase64(videoUrls, maxSize = 20) {
|
|
176
|
+
async function downloadVideosToTempFiles(videoUrls, tempDir, maxSize = 20) {
|
|
167
177
|
await import_promises.default.mkdir(tempDir, { recursive: true });
|
|
168
178
|
const results = [];
|
|
169
179
|
for (const url of videoUrls) {
|
|
170
180
|
let outputPath = null;
|
|
171
181
|
try {
|
|
172
|
-
outputPath = import_path.default.join(
|
|
182
|
+
outputPath = import_path.default.join(
|
|
183
|
+
tempDir,
|
|
184
|
+
`video_${Date.now()}_${crypto.randomUUID()}.mp4`
|
|
185
|
+
);
|
|
173
186
|
await new Promise((resolve, reject) => {
|
|
174
|
-
(0, import_fluent_ffmpeg.default)(url).inputOptions([
|
|
175
|
-
"-protocol_whitelist",
|
|
176
|
-
"file,http,https,tcp,tls,crypto"
|
|
177
|
-
]).outputOptions([
|
|
178
|
-
"-c",
|
|
179
|
-
"copy"
|
|
180
|
-
]).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();
|
|
181
188
|
});
|
|
182
189
|
const stats = await import_promises.default.stat(outputPath);
|
|
183
190
|
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
184
191
|
if (fileSizeInMB > maxSize) {
|
|
185
|
-
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);
|
|
186
196
|
} else {
|
|
187
|
-
|
|
188
|
-
const base64String = fileBuffer.toString("base64");
|
|
189
|
-
const dataUrl = `data:video/mp4;base64,${base64String}`;
|
|
190
|
-
results.push(dataUrl);
|
|
197
|
+
results.push(outputPath);
|
|
191
198
|
}
|
|
192
199
|
} catch (error) {
|
|
193
|
-
console.error(
|
|
200
|
+
console.error(`下载失败 (${url}):`, error?.message || error);
|
|
201
|
+
if (outputPath) await import_promises.default.unlink(outputPath).catch(() => {
|
|
202
|
+
});
|
|
194
203
|
results.push(null);
|
|
195
|
-
} finally {
|
|
196
|
-
if (outputPath) {
|
|
197
|
-
try {
|
|
198
|
-
await import_promises.default.unlink(outputPath);
|
|
199
|
-
} catch (deleteError) {
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
return results;
|
|
205
207
|
}
|
|
206
|
-
__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");
|
|
207
215
|
|
|
208
216
|
// src/taskqueue.ts
|
|
209
217
|
var taskQueue = class {
|
|
@@ -236,7 +244,7 @@ var taskQueue = class {
|
|
|
236
244
|
// src/index.tsx
|
|
237
245
|
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
238
246
|
var name = "nitter";
|
|
239
|
-
var inject = ["puppeteer", "subscription"
|
|
247
|
+
var inject = ["puppeteer", "subscription"];
|
|
240
248
|
var Config = import_koishi.Schema.intersect([
|
|
241
249
|
import_koishi.Schema.object({
|
|
242
250
|
apiKey: import_koishi.Schema.string().required().description("Twitter API Key"),
|
|
@@ -270,6 +278,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
270
278
|
app: import_koishi.Schema.string().description("subscription配置中应用名")
|
|
271
279
|
}).description("订阅配置"),
|
|
272
280
|
import_koishi.Schema.object({
|
|
281
|
+
enablePollingOnStart: import_koishi.Schema.boolean().default(true).description("启动后是否立刻开始轮询"),
|
|
273
282
|
enableReTweet: import_koishi.Schema.boolean().default(false).description("是否发送转推"),
|
|
274
283
|
sendPic: import_koishi.Schema.boolean().default(false).description("是否单独发送推文中的图片"),
|
|
275
284
|
cronString: import_koishi.Schema.string().default("15 */5 * * * *").description("使用cron表达式描述检查更新的时间,默认为每隔5分钟检查一次")
|
|
@@ -277,18 +286,12 @@ var Config = import_koishi.Schema.intersect([
|
|
|
277
286
|
import_koishi.Schema.union([
|
|
278
287
|
import_koishi.Schema.object({
|
|
279
288
|
sendPic: import_koishi.Schema.const(true).required(),
|
|
280
|
-
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和对接的应用都能访问")
|
|
281
291
|
})
|
|
282
292
|
])
|
|
283
293
|
]);
|
|
284
294
|
function apply(ctx, config) {
|
|
285
|
-
ctx.model.extend("nitter_records", {
|
|
286
|
-
id: "string",
|
|
287
|
-
createdAt: "timestamp"
|
|
288
|
-
}, {
|
|
289
|
-
primary: "id",
|
|
290
|
-
autoInc: false
|
|
291
|
-
});
|
|
292
295
|
config.nitterUrl = config.nitterUrl.replace(/\/+$/, "");
|
|
293
296
|
const twitterClient = new import_rettiwt_api.Rettiwt({
|
|
294
297
|
apiKey: config.apiKey,
|
|
@@ -303,14 +306,30 @@ function apply(ctx, config) {
|
|
|
303
306
|
} else if (config.enableTranslate == "openai") {
|
|
304
307
|
setOpenAiTranslate(config.baseurl, config.openaiApiKey, config.model, config.prompt, config.temperature, config.timeout);
|
|
305
308
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
333
|
})();
|
|
315
334
|
ctx.command("nitter.follow", "按照subscription中的订阅配置,使用登录的推特账号关注所有需要订阅的账号", { authority: 3 }).action(async () => {
|
|
316
335
|
const whiteList = ctx.subscription.getAvailableAccounts(config.app);
|
|
@@ -321,12 +340,13 @@ function apply(ctx, config) {
|
|
|
321
340
|
let user;
|
|
322
341
|
await retry(3, async () => {
|
|
323
342
|
user = await twitterClient.user.details(id);
|
|
324
|
-
});
|
|
343
|
+
}, (err) => ctx.logger("nitter").error(`获取用户${id}信息失败: ${err}`));
|
|
344
|
+
if (!user) continue;
|
|
325
345
|
await new Promise((resolve) => setTimeout(resolve, 3 * 1e3));
|
|
326
346
|
await retry(3, async () => {
|
|
327
347
|
await twitterClient.user.follow(user.id);
|
|
328
|
-
|
|
329
|
-
|
|
348
|
+
ctx.logger("nitter").info(`关注${id}成功`);
|
|
349
|
+
}, (err) => ctx.logger("nitter").error(`关注${id}失败: ${err}`));
|
|
330
350
|
await new Promise((resolve) => setTimeout(resolve, 30 * 1e3));
|
|
331
351
|
}
|
|
332
352
|
}
|
|
@@ -337,17 +357,11 @@ function apply(ctx, config) {
|
|
|
337
357
|
return "请输入推文ID";
|
|
338
358
|
}
|
|
339
359
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
forwardMsg.push(...imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })));
|
|
346
|
-
}
|
|
347
|
-
if (videoUrls.length > 0) {
|
|
348
|
-
forwardMsg.push(...videoUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { src: url }) })));
|
|
349
|
-
}
|
|
350
|
-
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
|
+
);
|
|
351
365
|
} catch (error) {
|
|
352
366
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
353
367
|
return "获取推文失败";
|
|
@@ -359,53 +373,79 @@ function apply(ctx, config) {
|
|
|
359
373
|
}
|
|
360
374
|
queue.push({ account, tweetId });
|
|
361
375
|
});
|
|
362
|
-
async function
|
|
363
|
-
|
|
376
|
+
async function work(tweetId, sendFunc, sendForwardFunc) {
|
|
377
|
+
let videoFiles = [];
|
|
364
378
|
try {
|
|
365
|
-
const
|
|
379
|
+
const { screenshot, pieces } = await renderTweetScreenshot(tweetId);
|
|
366
380
|
const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
const videoUrls =
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
387
|
}
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
375
406
|
}
|
|
376
407
|
if (forwardMsg.length > 0) {
|
|
377
|
-
await
|
|
408
|
+
await sendForwardFunc(forwardMsg);
|
|
378
409
|
}
|
|
379
410
|
} catch (error) {
|
|
380
411
|
ctx.logger("nitter").error("获取推文失败:", error);
|
|
412
|
+
} finally {
|
|
413
|
+
await cleanupTempFiles(videoFiles);
|
|
381
414
|
}
|
|
382
415
|
}
|
|
383
|
-
__name(
|
|
416
|
+
__name(work, "work");
|
|
384
417
|
async function getFollowedFeed() {
|
|
385
418
|
let tweetList;
|
|
386
|
-
await retry(
|
|
419
|
+
await retry(5, async () => {
|
|
387
420
|
({ list: tweetList } = await twitterClient.user.followed());
|
|
388
|
-
});
|
|
421
|
+
}, (err) => ctx.logger("nitter").error(`获取推文列表失败: ${err}`));
|
|
422
|
+
if (!tweetList) return [];
|
|
389
423
|
return tweetList.reverse();
|
|
390
424
|
}
|
|
391
425
|
__name(getFollowedFeed, "getFollowedFeed");
|
|
426
|
+
let checking = false;
|
|
392
427
|
async function checkForUpdates() {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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;
|
|
409
449
|
}
|
|
410
450
|
}
|
|
411
451
|
__name(checkForUpdates, "checkForUpdates");
|
|
@@ -419,13 +459,48 @@ function apply(ctx, config) {
|
|
|
419
459
|
await page.setCacheEnabled(false);
|
|
420
460
|
const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
|
|
421
461
|
await retry(3, async () => {
|
|
422
|
-
await page.goto(tweetUrl);
|
|
462
|
+
await page.goto(tweetUrl, { timeout: 12e4 });
|
|
423
463
|
const element2 = await page.$(".main-thread");
|
|
424
464
|
if (!element2) throw new Error("Rate Limited");
|
|
465
|
+
}, (err) => {
|
|
466
|
+
throw new Error(`获取推文${tweetId}失败: ${err}`);
|
|
425
467
|
}, 2e3);
|
|
426
|
-
|
|
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") {
|
|
427
501
|
try {
|
|
428
|
-
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;
|
|
429
504
|
} catch (e) {
|
|
430
505
|
ctx.logger("nitter").info("翻译失败", e);
|
|
431
506
|
}
|
|
@@ -469,18 +544,19 @@ function apply(ctx, config) {
|
|
|
469
544
|
omitBackground: false,
|
|
470
545
|
clip: boundingBox
|
|
471
546
|
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
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
|
+
}
|
|
482
558
|
}
|
|
483
|
-
return
|
|
559
|
+
return { screenshot: buffer, pieces };
|
|
484
560
|
} catch (e) {
|
|
485
561
|
throw e;
|
|
486
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",
|