koishi-plugin-nitter 0.0.1

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/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Context, Dict, Schema } from 'koishi';
2
+ export declare const name = "nitter";
3
+ export declare const inject: string[];
4
+ export interface Config {
5
+ apiKey: string;
6
+ nitterUrl: string;
7
+ proxy: string;
8
+ enableTranslate: "google" | "silicon" | "disable";
9
+ googleApiKey?: string;
10
+ siliconApiKey?: string;
11
+ model?: string;
12
+ prompt?: string;
13
+ app: string;
14
+ sendPic: boolean;
15
+ enableReTweet: boolean;
16
+ }
17
+ export declare const Config: Schema<Config, Dict>;
18
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,347 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.tsx
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ Config: () => Config,
34
+ apply: () => apply,
35
+ inject: () => inject,
36
+ name: () => name
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+ var import_koishi = require("koishi");
40
+
41
+ // src/translate.ts
42
+ var import_axios = __toESM(require("axios"));
43
+ var import_https_proxy_agent = require("https-proxy-agent");
44
+ async function retry(retries, fn, delay = 500) {
45
+ try {
46
+ await fn();
47
+ return Promise.resolve();
48
+ } catch (err) {
49
+ console.log(`剩余${retries}次尝试`, err);
50
+ await new Promise((resolve) => setTimeout(resolve, delay));
51
+ return retries > 1 ? retry(retries - 1, fn, delay * 2) : Promise.reject(err);
52
+ }
53
+ ;
54
+ }
55
+ __name(retry, "retry");
56
+ var translate;
57
+ function setGoogleTranslate(key, proxy) {
58
+ translate = /* @__PURE__ */ __name((texts) => {
59
+ const url = `https://translation.googleapis.com/language/translate/v2?key=${key}`;
60
+ return Promise.all(texts.map(async (text) => {
61
+ let data;
62
+ await retry(3, async () => {
63
+ ({ data } = await import_axios.default.post(url, {
64
+ q: text.split("\n"),
65
+ target: "zh"
66
+ }, {
67
+ httpsAgent: new import_https_proxy_agent.HttpsProxyAgent(proxy),
68
+ proxy: false
69
+ }));
70
+ });
71
+ return data.data.translations.map((item) => item.translatedText).join("\n");
72
+ }));
73
+ }, "translate");
74
+ }
75
+ __name(setGoogleTranslate, "setGoogleTranslate");
76
+ function setSiliconTranslate(key, model, prompt) {
77
+ translate = /* @__PURE__ */ __name(async (texts) => {
78
+ const url = "https://api.siliconflow.cn/v1/chat/completions";
79
+ let data;
80
+ await retry(3, async () => {
81
+ const response = await (0, import_axios.default)({
82
+ method: "POST",
83
+ url,
84
+ headers: {
85
+ "Authorization": `Bearer ${key}`,
86
+ "Content-Type": "application/json"
87
+ },
88
+ data: {
89
+ model: "deepseek-ai/DeepSeek-V3.2-Exp",
90
+ messages: [
91
+ {
92
+ role: "system",
93
+ content: prompt
94
+ },
95
+ {
96
+ role: "user",
97
+ content: `请翻译以下${texts.length}条HTML内容,返回JSON数组:
98
+
99
+ ${JSON.stringify(texts)}`
100
+ }
101
+ ],
102
+ stream: false,
103
+ temperature: 0.3,
104
+ // 较低的温度值使翻译更稳定
105
+ response_format: { type: "json_object" }
106
+ },
107
+ timeout: 3e4
108
+ // 30秒超时
109
+ });
110
+ if (response.data && response.data.choices && response.data.choices[0]) {
111
+ const content = response.data.choices[0].message.content;
112
+ data = JSON.parse(content);
113
+ if (!Array.isArray(data)) throw new Error("模型未返回数组");
114
+ } else {
115
+ throw new Error("API返回数据格式异常");
116
+ }
117
+ });
118
+ return data;
119
+ }, "translate");
120
+ }
121
+ __name(setSiliconTranslate, "setSiliconTranslate");
122
+ async function addTranslate(page, className) {
123
+ const elementsHTML = await page.evaluate((className2) => {
124
+ const elements = document.querySelectorAll(className2);
125
+ if (!elements.length) return null;
126
+ return Array.from(elements).map((element) => element.outerHTML);
127
+ }, className);
128
+ if (!elementsHTML || !elementsHTML.length) return;
129
+ const translatedHTMLs = await translate(elementsHTML);
130
+ await page.evaluate((className2, originalHTMLs, translatedHTMLs2) => {
131
+ const elements = document.querySelectorAll(className2);
132
+ elements.forEach((element, index) => {
133
+ if (element.outerHTML === originalHTMLs[index]) {
134
+ const tempDiv = document.createElement("div");
135
+ tempDiv.innerHTML = translatedHTMLs2[index];
136
+ const newElement = tempDiv.firstElementChild;
137
+ if (newElement) {
138
+ element.insertAdjacentElement("afterend", newElement);
139
+ element.insertAdjacentHTML("afterend", '<div style="height: 1px;background-color: #ccc; margin-top: 10px; margin-bottom: 10px;"></div>');
140
+ }
141
+ }
142
+ });
143
+ }, className, elementsHTML, translatedHTMLs);
144
+ }
145
+ __name(addTranslate, "addTranslate");
146
+
147
+ // src/index.tsx
148
+ var import_rettiwt_api = require("rettiwt-api");
149
+ var import_node_cron = require("node-cron");
150
+ var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
151
+ var name = "nitter";
152
+ var inject = ["puppeteer", "subscription"];
153
+ var Config = import_koishi.Schema.intersect([
154
+ import_koishi.Schema.object({
155
+ apiKey: import_koishi.Schema.string().required().description("Twitter API Key"),
156
+ nitterUrl: import_koishi.Schema.string().required().description("Nitter"),
157
+ proxy: import_koishi.Schema.string().description("代理设置")
158
+ }).description("API 配置"),
159
+ import_koishi.Schema.object({
160
+ enableTranslate: import_koishi.Schema.union([
161
+ import_koishi.Schema.const("google").description("google cloud translation"),
162
+ import_koishi.Schema.const("silicon").description("硅基流动"),
163
+ import_koishi.Schema.const("disable").description("关闭")
164
+ ]).default("disable").role("radio")
165
+ }).description("翻译设置"),
166
+ import_koishi.Schema.union([
167
+ import_koishi.Schema.object({
168
+ enableTranslate: import_koishi.Schema.const("google").required(),
169
+ googleApiKey: import_koishi.Schema.string().required().description("访问https://cloud.google.com/获取,使用v2")
170
+ }),
171
+ import_koishi.Schema.object({
172
+ enableTranslate: import_koishi.Schema.const("silicon").required(),
173
+ siliconApiKey: import_koishi.Schema.string().required().description("访问https://www.siliconflow.cn/获取"),
174
+ model: import_koishi.Schema.string().required().description("模型名称"),
175
+ prompt: import_koishi.Schema.string().required().default("你是一个专业的HTML翻译助手。请将一个HTML数组翻译为中文,严格遵循以下规则:\n1. 翻译文本内容,包括链接标签(如<a>)内的显示文本\n2.保持所有HTML标签、属性、class、id,以及结构和格式,URL链接完全不变\n3. 返回一个严格的由html文本组成的JSON数组,包含与输入数量完全相同的翻译结果,不要添加任何额外说明").role("textarea").description("提示词")
176
+ }),
177
+ import_koishi.Schema.object({})
178
+ ]),
179
+ import_koishi.Schema.object({
180
+ app: import_koishi.Schema.string().description("subscription配置中应用名")
181
+ }).description("订阅配置"),
182
+ import_koishi.Schema.object({
183
+ sendPic: import_koishi.Schema.boolean().default(false).description("是否单独发送推文中的图片"),
184
+ enableReTweet: import_koishi.Schema.boolean().default(false).description("是否发送转推")
185
+ }).description("推送设置")
186
+ ]);
187
+ function apply(ctx, config) {
188
+ config.nitterUrl = config.nitterUrl.replace(/\/+$/, "");
189
+ const twitterClient = new import_rettiwt_api.Rettiwt({
190
+ apiKey: config.apiKey,
191
+ proxyUrl: config.proxy ? new URL(config.proxy) : void 0
192
+ });
193
+ let latestTweetId;
194
+ let cronJob;
195
+ (async () => {
196
+ if (config.enableTranslate == "google") {
197
+ setGoogleTranslate(config.googleApiKey, config.proxy);
198
+ } else if (config.enableTranslate == "silicon") {
199
+ setSiliconTranslate(config.siliconApiKey, config.model, config.prompt);
200
+ }
201
+ const tweetList = await getFollowedFeed();
202
+ for (const data of tweetList) {
203
+ latestTweetId ||= "";
204
+ if (latestTweetId < data.id)
205
+ latestTweetId = data.id;
206
+ }
207
+ cronJob = (0, import_node_cron.schedule)("15 * * * * *", checkForUpdates);
208
+ ctx.on("dispose", () => {
209
+ cronJob.stop();
210
+ });
211
+ ctx.logger("nitter").info("开始监听推特动态");
212
+ })();
213
+ ctx.command("nitter.follow", "按照subscription中的订阅配置,使用登录的推特账号关注所有需要订阅的账号", { authority: 3 }).action(async () => {
214
+ const whiteList = ctx.subscription.getAvailableAccounts(config.app);
215
+ const { list: followingList } = await twitterClient.user.following();
216
+ const followingIdList = followingList.map((user) => user.userName);
217
+ for (const id of whiteList) {
218
+ if (!followingIdList.includes(id)) {
219
+ const user = await twitterClient.user.details(id);
220
+ await new Promise((resolve) => setTimeout(resolve, 3 * 1e3));
221
+ await twitterClient.user.follow(user.id);
222
+ console.log(`关注${id}成功`);
223
+ await new Promise((resolve) => setTimeout(resolve, 30 * 1e3));
224
+ }
225
+ }
226
+ return "关注完成";
227
+ });
228
+ ctx.command("nitter <tweetId:string>", "根据推文ID获取推文").action(async ({ session }, tweetId) => {
229
+ if (!tweetId) {
230
+ return "请输入推文ID";
231
+ }
232
+ try {
233
+ const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
234
+ let msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
235
+ if (imageUrls.length > 0) {
236
+ msg += /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) })) });
237
+ }
238
+ return msg;
239
+ } catch (error) {
240
+ ctx.logger("nitter").error("获取推文失败:", error);
241
+ return "获取推文失败";
242
+ }
243
+ });
244
+ ctx.command("nitter.broadcast <account:string> <tweetId:string>", "根据账号和推文ID推送推文", { authority: 3 }).action(async ({ session }, account, tweetId) => {
245
+ if (!tweetId) {
246
+ return "请输入推文ID";
247
+ }
248
+ broadcast(account, tweetId);
249
+ });
250
+ async function broadcast(account, tweetId) {
251
+ try {
252
+ console.log(1);
253
+ const [screenshot, imageUrls] = await renderTweetScreenshot(ctx, tweetId, config);
254
+ const screenshotMsg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: "data:image/png;base64," + screenshot.toString("base64") });
255
+ ctx.subscription.broadcast(config.app, account, screenshotMsg);
256
+ if (imageUrls.length > 0) {
257
+ const picsForward = imageUrls.map((url) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: url }) }));
258
+ ctx.subscription.broadcastForward(config.app, account, picsForward);
259
+ }
260
+ } catch (error) {
261
+ ctx.logger("twitter").error("获取推文失败:", error);
262
+ }
263
+ }
264
+ __name(broadcast, "broadcast");
265
+ async function getFollowedFeed() {
266
+ let tweetList;
267
+ await retry(3, async () => {
268
+ ({ list: tweetList } = await twitterClient.user.followed());
269
+ });
270
+ return tweetList.reverse();
271
+ }
272
+ __name(getFollowedFeed, "getFollowedFeed");
273
+ async function checkForUpdates() {
274
+ const tweetList = await getFollowedFeed();
275
+ for (const data of tweetList) {
276
+ if (!config.enableReTweet && data.retweetedTweet)
277
+ continue;
278
+ if (data.id <= latestTweetId)
279
+ continue;
280
+ if (!ctx.subscription.getAvailableAccounts(config.app).includes(data.tweetBy.userName))
281
+ continue;
282
+ latestTweetId = data.id;
283
+ ctx.logger("nitter").info(`检测到推文id:${data.id},开始推送`);
284
+ await broadcast(data.tweetBy.userName, data.id);
285
+ }
286
+ }
287
+ __name(checkForUpdates, "checkForUpdates");
288
+ }
289
+ __name(apply, "apply");
290
+ async function renderTweetScreenshot(ctx, tweetId, config) {
291
+ const puppeteer = ctx.puppeteer;
292
+ if (!puppeteer) {
293
+ throw new Error("Puppeteer 服务未找到,请安装 koishi-plugin-puppeteer");
294
+ }
295
+ const page = await puppeteer.page();
296
+ try {
297
+ const tweetUrl = `${config.nitterUrl}/i/status/${tweetId}`;
298
+ await retry(3, async () => {
299
+ await page.goto(tweetUrl);
300
+ const element2 = await page.$(".main-thread");
301
+ if (!element2) throw new Error("Rate Limited");
302
+ }, 2e3);
303
+ if (config.enableTranslate) {
304
+ await addTranslate(page, ".main-thread .tweet-content, .main-thread .quote-text");
305
+ }
306
+ await new Promise((resolve) => setTimeout(resolve, 200));
307
+ await page.evaluate(() => {
308
+ const nav = document.querySelector("nav");
309
+ if (nav) {
310
+ nav.style.visibility = "hidden";
311
+ }
312
+ });
313
+ const element = await page.$(".main-thread");
314
+ const buffer = await element.screenshot({
315
+ // 不指定path参数,直接返回buffer
316
+ type: "png",
317
+ omitBackground: false
318
+ });
319
+ if (config.sendPic) {
320
+ const originalImages = await page.evaluate((url) => {
321
+ const stillImageLinks = document.querySelectorAll(".main-tweet a.still-image");
322
+ const imageUrls = [];
323
+ stillImageLinks.forEach((link) => {
324
+ const href = link.getAttribute("href");
325
+ if (href) {
326
+ imageUrls.push(`${url}${href}`);
327
+ }
328
+ });
329
+ return imageUrls;
330
+ }, config.nitterUrl);
331
+ return [buffer, originalImages];
332
+ }
333
+ return [buffer, []];
334
+ } catch (e) {
335
+ throw e;
336
+ } finally {
337
+ await page.close();
338
+ }
339
+ }
340
+ __name(renderTweetScreenshot, "renderTweetScreenshot");
341
+ // Annotate the CommonJS export names for ESM import in node:
342
+ 0 && (module.exports = {
343
+ Config,
344
+ apply,
345
+ inject,
346
+ name
347
+ });
@@ -0,0 +1,5 @@
1
+ import { Page } from 'puppeteer-core';
2
+ export declare function retry(retries: number, fn: () => any, delay?: number): any;
3
+ export declare function setGoogleTranslate(key: string, proxy: string): void;
4
+ export declare function setSiliconTranslate(key: string, model: string, prompt: string): void;
5
+ export declare function addTranslate(page: Page, className: string): Promise<void>;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "koishi-plugin-nitter",
3
+ "description": "使用Rettiwt-API订阅推文,并使用nitter渲染",
4
+ "version": "0.0.1",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin"
16
+ ],
17
+ "peerDependencies": {
18
+ "@satorijs/element": "^3.1.8",
19
+ "axios": "^1.12.2",
20
+ "koishi": "^4.18.9",
21
+ "koishi-plugin-subscription": "^0.0.1",
22
+ "rettiwt-api": "^6.0.6",
23
+ "https-proxy-agent": "^7.0.6",
24
+ "node-cron": "^4.2.1"
25
+ }
26
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-twitter
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-twitter?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-twitter)
4
+
5
+ 推文订阅