koishi-plugin-vr-fever 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/README.md +21 -0
- package/lib/commands/weibo.d.ts +7 -0
- package/lib/commands/weibo.js +177 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +110 -0
- package/lib/service/comment/fetch-for-timeline.d.ts +4 -0
- package/lib/service/comment/fetch-for-timeline.js +41 -0
- package/lib/service/comment/filter.d.ts +3 -0
- package/lib/service/comment/filter.js +25 -0
- package/lib/service/comment/formatter.d.ts +3 -0
- package/lib/service/comment/formatter.js +42 -0
- package/lib/service/comment/normalizer.d.ts +2 -0
- package/lib/service/comment/normalizer.js +53 -0
- package/lib/service/comment/types.d.ts +23 -0
- package/lib/service/comment/types.js +2 -0
- package/lib/service/drawer/html-builder.d.ts +6 -0
- package/lib/service/drawer/html-builder.js +510 -0
- package/lib/service/drawer/image-resolver.d.ts +10 -0
- package/lib/service/drawer/image-resolver.js +174 -0
- package/lib/service/drawer/index.d.ts +4 -0
- package/lib/service/drawer/index.js +12 -0
- package/lib/service/drawer/screenshot.d.ts +7 -0
- package/lib/service/drawer/screenshot.js +75 -0
- package/lib/service/drawer/types.d.ts +42 -0
- package/lib/service/drawer/types.js +2 -0
- package/lib/service/login.d.ts +4 -0
- package/lib/service/login.js +79 -0
- package/lib/service/poll.d.ts +8 -0
- package/lib/service/poll.js +100 -0
- package/lib/service/timeline/index.d.ts +3 -0
- package/lib/service/timeline/index.js +19 -0
- package/lib/service/timeline/normalizer.d.ts +18 -0
- package/lib/service/timeline/normalizer.js +141 -0
- package/lib/service/timeline/text-formatter.d.ts +3 -0
- package/lib/service/timeline/text-formatter.js +114 -0
- package/lib/service/timeline/types.d.ts +72 -0
- package/lib/service/timeline/types.js +9 -0
- package/lib/service/weibo-fetch.d.ts +14 -0
- package/lib/service/weibo-fetch.js +66 -0
- package/lib/service/weibo-http.d.ts +3 -0
- package/lib/service/weibo-http.js +27 -0
- package/lib/util/constants.d.ts +18 -0
- package/lib/util/constants.js +43 -0
- package/lib/util/html.d.ts +1 -0
- package/lib/util/html.js +10 -0
- package/lib/util/parse-render-output.d.ts +2 -0
- package/lib/util/parse-render-output.js +13 -0
- package/lib/util/puppeteer-cookie.d.ts +47 -0
- package/lib/util/puppeteer-cookie.js +552 -0
- package/lib/util/save-screenshot.d.ts +14 -0
- package/lib/util/save-screenshot.js +26 -0
- package/lib/util/send-msg.d.ts +4 -0
- package/lib/util/send-msg.js +12 -0
- package/lib/util/timer.d.ts +2 -0
- package/lib/util/timer.js +10 -0
- package/lib/util/weibo-date.d.ts +4 -0
- package/lib/util/weibo-date.js +47 -0
- package/lib/util/weibo-media.d.ts +8 -0
- package/lib/util/weibo-media.js +26 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
我就跟你实话实说了
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# koishi-plugin-vr-fever
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/koishi-plugin-vr-fever)
|
|
7
|
+
|
|
8
|
+
自用改型回旋插件
|
|
9
|
+
|
|
10
|
+
## weibo相关
|
|
11
|
+
|
|
12
|
+
期间内同博主多条微博&点赞(按发布时间预估点赞)
|
|
13
|
+
|
|
14
|
+
照搬了以下仓库,进行魔改。如果你追求更好的使用体验,请使用它们:
|
|
15
|
+
|
|
16
|
+
- https://github.com/moehuhu/weibo-monitor#readme
|
|
17
|
+
- https://github.com/MingxiaGuo/koishi-plugin-weibo-notify#readme
|
|
18
|
+
|
|
19
|
+
## 其他
|
|
20
|
+
|
|
21
|
+
tbd
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Context } from "koishi";
|
|
2
|
+
import type Puppeteer from "koishi-plugin-puppeteer";
|
|
3
|
+
import type { Config } from "../index";
|
|
4
|
+
export interface WeiboContext extends Context {
|
|
5
|
+
puppeteer: Puppeteer;
|
|
6
|
+
}
|
|
7
|
+
export declare const registerWeiboCommand: (ctx: WeiboContext, config: Config, pollWeibo: () => Promise<void>) => void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerWeiboCommand = void 0;
|
|
4
|
+
const login_1 = require("../service/login");
|
|
5
|
+
// import { formatCommentsMessages } from "../service/comment/formatter";
|
|
6
|
+
// import { getWeiboCommentsByWeiboID } from "../service/weibo-fetch";
|
|
7
|
+
const constants_1 = require("../util/constants");
|
|
8
|
+
const puppeteer_cookie_1 = require("../util/puppeteer-cookie");
|
|
9
|
+
const send_msg_1 = require("../util/send-msg");
|
|
10
|
+
const registerWeiboCommand = (ctx, config, pollWeibo) => {
|
|
11
|
+
ctx.command("weibo <message>").action(async (argv, message) => {
|
|
12
|
+
if (!ctx?.puppeteer) {
|
|
13
|
+
return "please install puppeteer plugin";
|
|
14
|
+
}
|
|
15
|
+
if (message === "list") {
|
|
16
|
+
const groupID = (argv.session?.channel)
|
|
17
|
+
.id;
|
|
18
|
+
const subscribes = await ctx.database
|
|
19
|
+
.select("weibo_subscribes")
|
|
20
|
+
.where({ groupID, isActive: true })
|
|
21
|
+
.execute();
|
|
22
|
+
if (!subscribes) {
|
|
23
|
+
return argv.session.sendQueued("未找到订阅");
|
|
24
|
+
}
|
|
25
|
+
const msg = ["当前订阅的UID:"];
|
|
26
|
+
Array.from(subscribes).forEach((subscribe) => {
|
|
27
|
+
msg.push(`- ${subscribe.isActive ? "💚" : "🩶"} ${subscribe.weiboUID} ${subscribe.weiboName} `);
|
|
28
|
+
});
|
|
29
|
+
await argv.session.sendQueued(msg.join("\n"));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (message === "add") {
|
|
33
|
+
let [, weiboUID] = argv.args;
|
|
34
|
+
weiboUID = weiboUID?.trim();
|
|
35
|
+
if (!weiboUID || !constants_1.REGEX.IS_WEIBO_UID.test(weiboUID)) {
|
|
36
|
+
return argv.session.sendQueued("请输入正确格式的UID");
|
|
37
|
+
}
|
|
38
|
+
const groupID = (argv.session?.channel)
|
|
39
|
+
.id;
|
|
40
|
+
if (!groupID) {
|
|
41
|
+
return argv.session.sendQueued("未找到群ID");
|
|
42
|
+
}
|
|
43
|
+
const beforeSubscribe = await ctx.database
|
|
44
|
+
.select("weibo_subscribes")
|
|
45
|
+
.where({ id: `${weiboUID}-${groupID}` })
|
|
46
|
+
.execute();
|
|
47
|
+
if (beforeSubscribe.length > 0) {
|
|
48
|
+
return argv.session.sendQueued("已订阅,无需重复订阅");
|
|
49
|
+
}
|
|
50
|
+
await ctx.database.create("weibo_subscribes", {
|
|
51
|
+
id: `${weiboUID}-${groupID}`,
|
|
52
|
+
weiboUID,
|
|
53
|
+
groupID,
|
|
54
|
+
isActive: true,
|
|
55
|
+
createdAt: new Date(),
|
|
56
|
+
});
|
|
57
|
+
// const bot = ctx.bots[`onebot:${config.adminAccount.trim()}`];
|
|
58
|
+
// return bot.sendMessage(groupID, `订阅成功: ${weiboUID}`);
|
|
59
|
+
argv.session.sendQueued(`订阅成功: ${weiboUID}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (message === "remove") {
|
|
63
|
+
let [, weiboUID] = argv.args;
|
|
64
|
+
weiboUID = weiboUID?.trim();
|
|
65
|
+
if (!weiboUID || !constants_1.REGEX.IS_WEIBO_UID.test(weiboUID)) {
|
|
66
|
+
return argv.session.sendQueued("请输入正确格式的UID");
|
|
67
|
+
}
|
|
68
|
+
const groupID = (argv.session?.channel)
|
|
69
|
+
.id;
|
|
70
|
+
if (!groupID) {
|
|
71
|
+
return argv.session.sendQueued("未找到群ID");
|
|
72
|
+
}
|
|
73
|
+
const beforeSubscribe = await ctx.database
|
|
74
|
+
.select("weibo_subscribes")
|
|
75
|
+
.where({ id: `${weiboUID}-${groupID}` })
|
|
76
|
+
.execute();
|
|
77
|
+
if (beforeSubscribe.length === 0) {
|
|
78
|
+
return argv.session.sendQueued("未找到订阅,无需取消订阅");
|
|
79
|
+
}
|
|
80
|
+
argv.session.sendQueued(`正在取消订阅.../nUID: ${weiboUID} ${beforeSubscribe[0].weiboName}`);
|
|
81
|
+
await ctx.database.set("weibo_subscribes", {
|
|
82
|
+
weiboUID,
|
|
83
|
+
groupID,
|
|
84
|
+
}, {
|
|
85
|
+
isActive: false,
|
|
86
|
+
});
|
|
87
|
+
// const bot = ctx.bots[`onebot:${config.adminAccount.trim()}`];
|
|
88
|
+
// return bot.sendMessage(groupID, `移除订阅成功: ${weiboUID}`);
|
|
89
|
+
argv.session.sendQueued(`移除订阅成功: ${weiboUID}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (message === "help") {
|
|
93
|
+
(0, send_msg_1.sendMsg)([
|
|
94
|
+
"weibo 命令帮助:",
|
|
95
|
+
"💚",
|
|
96
|
+
"help - 显示本帮助",
|
|
97
|
+
"list - 查看本群订阅列表",
|
|
98
|
+
"add <UID> - 订阅博主",
|
|
99
|
+
"remove <UID> - 取消订阅",
|
|
100
|
+
"login - 扫码登录微博",
|
|
101
|
+
"pull - 立即拉取一次微博卡片",
|
|
102
|
+
"💚",
|
|
103
|
+
"weibo list",
|
|
104
|
+
"weibo add 8376019184",
|
|
105
|
+
"weibo remove 1234567890",
|
|
106
|
+
"weibo login",
|
|
107
|
+
"weibo pull",
|
|
108
|
+
].join("\n"), argv.session);
|
|
109
|
+
}
|
|
110
|
+
console.log(message);
|
|
111
|
+
if (message === "login") {
|
|
112
|
+
try {
|
|
113
|
+
await (0, puppeteer_cookie_1.ensurePuppeteerBrowser)(ctx);
|
|
114
|
+
await (0, login_1.getQRcode)(ctx, argv.session);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
(0, send_msg_1.sendMsg)((0, puppeteer_cookie_1.formatPuppeteerError)(error), argv.session);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// if (message === "cookie") {
|
|
121
|
+
// const cookies = await loadCookiesFromDatabase(ctx);
|
|
122
|
+
// if (!cookies || cookies.length === 0) {
|
|
123
|
+
// return argv.session.sendQueued("no cookies found");
|
|
124
|
+
// }
|
|
125
|
+
// return argv.session.sendQueued(JSON.stringify(cookies));
|
|
126
|
+
// }
|
|
127
|
+
// if (message === "comments") {
|
|
128
|
+
// try {
|
|
129
|
+
// let [, weiboUID, weiboID] = argv.args;
|
|
130
|
+
// weiboUID = weiboUID?.trim();
|
|
131
|
+
// weiboID = weiboID?.trim();
|
|
132
|
+
// if (
|
|
133
|
+
// !weiboUID ||
|
|
134
|
+
// !REGEX.IS_WEIBO_UID.test(weiboUID) ||
|
|
135
|
+
// !weiboID ||
|
|
136
|
+
// !/^\d+$/.test(weiboID)
|
|
137
|
+
// ) {
|
|
138
|
+
// return argv.session.sendQueued("请输入正确格式的UID和微博ID");
|
|
139
|
+
// }
|
|
140
|
+
// const result = await getWeiboCommentsByWeiboID(weiboID, weiboUID, ctx);
|
|
141
|
+
// if (!result?.comments.length) {
|
|
142
|
+
// return argv.session.sendQueued("未找到评论数据");
|
|
143
|
+
// }
|
|
144
|
+
// for (const msg of formatCommentsMessages(result)) {
|
|
145
|
+
// await argv.session.sendQueued(msg);
|
|
146
|
+
// }
|
|
147
|
+
// return;
|
|
148
|
+
// } catch (error: any) {
|
|
149
|
+
// return argv.session.sendQueued(error.message);
|
|
150
|
+
// }
|
|
151
|
+
// }
|
|
152
|
+
if (message === "立刻获取") {
|
|
153
|
+
try {
|
|
154
|
+
await pollWeibo();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return argv.session.sendQueued((0, puppeteer_cookie_1.formatPuppeteerError)(error));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (message === "check") {
|
|
161
|
+
try {
|
|
162
|
+
const loginStatus = await (0, login_1.checkLoginStatus)(ctx);
|
|
163
|
+
if (loginStatus) {
|
|
164
|
+
return argv.session.sendQueued("微博登录状态正常");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
return argv.session.sendQueued("微博登录状态异常");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return argv.session.sendQueued((0, puppeteer_cookie_1.formatPuppeteerError)(error));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
exports.registerWeiboCommand = registerWeiboCommand;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context, Schema } from "koishi";
|
|
2
|
+
export declare const name = "vr-fever";
|
|
3
|
+
export interface WeiboCookie {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
updatedAt: Date;
|
|
8
|
+
}
|
|
9
|
+
interface WeiboSubscribe {
|
|
10
|
+
id: string;
|
|
11
|
+
weiboUID: string;
|
|
12
|
+
groupID: string;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
weiboName?: string;
|
|
16
|
+
remark?: string;
|
|
17
|
+
}
|
|
18
|
+
declare module "koishi" {
|
|
19
|
+
interface Tables {
|
|
20
|
+
weibo_cookies: WeiboCookie;
|
|
21
|
+
weibo_subscribes: WeiboSubscribe;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export declare const using: string[];
|
|
25
|
+
export declare const inject: {
|
|
26
|
+
required: string[];
|
|
27
|
+
optional: string[];
|
|
28
|
+
};
|
|
29
|
+
export interface Config {
|
|
30
|
+
adminAccount: string;
|
|
31
|
+
adminGroupID: string;
|
|
32
|
+
waitMinutes: number;
|
|
33
|
+
isTextMode: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare const Config: Schema<Config>;
|
|
36
|
+
export declare function apply(ctx: Context, config: Config): Promise<void>;
|
|
37
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.inject = exports.using = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const weibo_1 = require("./commands/weibo");
|
|
7
|
+
const poll_1 = require("./service/poll");
|
|
8
|
+
// import { ensurePuppeteerBrowser } from "./util/puppeteer-cookie";
|
|
9
|
+
const timer_1 = require("./util/timer");
|
|
10
|
+
const login_1 = require("./service/login");
|
|
11
|
+
exports.name = "vr-fever";
|
|
12
|
+
exports.using = ["puppeteer", "database", "http"];
|
|
13
|
+
exports.inject = {
|
|
14
|
+
required: [...exports.using],
|
|
15
|
+
optional: ["console", "server"],
|
|
16
|
+
};
|
|
17
|
+
exports.Config = koishi_1.Schema.object({
|
|
18
|
+
adminAccount: koishi_1.Schema.string().description("账号(qq号)"),
|
|
19
|
+
adminGroupID: koishi_1.Schema.string().description("管理员群ID,用于微博是否掉登录"),
|
|
20
|
+
waitMinutes: koishi_1.Schema.number()
|
|
21
|
+
.default(3)
|
|
22
|
+
.min(3)
|
|
23
|
+
.description("隔多久拉取一次最新微博 (分钟),最少3分钟"),
|
|
24
|
+
isTextMode: koishi_1.Schema.boolean()
|
|
25
|
+
.default(false)
|
|
26
|
+
.description("开启后以文本推送微博,关闭则以截图图片推送"),
|
|
27
|
+
});
|
|
28
|
+
async function apply(ctx, config) {
|
|
29
|
+
ctx.model.extend("weibo_cookies", {
|
|
30
|
+
name: "string",
|
|
31
|
+
value: "string",
|
|
32
|
+
domain: "string",
|
|
33
|
+
updatedAt: "timestamp",
|
|
34
|
+
}, {
|
|
35
|
+
primary: ["name", "domain"],
|
|
36
|
+
});
|
|
37
|
+
ctx.model.extend("weibo_subscribes", {
|
|
38
|
+
id: "string",
|
|
39
|
+
weiboUID: "string",
|
|
40
|
+
groupID: "string",
|
|
41
|
+
createdAt: "timestamp",
|
|
42
|
+
isActive: "boolean",
|
|
43
|
+
weiboName: "string",
|
|
44
|
+
remark: "string",
|
|
45
|
+
}, {
|
|
46
|
+
primary: ["id"],
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* 创建一个用于发送消息到OneBot的会话对象
|
|
50
|
+
* @param groupId - 目标群组的ID
|
|
51
|
+
* @returns 返回一个Session对象,包含发送消息的方法
|
|
52
|
+
*/
|
|
53
|
+
const sendMsgOnebot = (groupId) => {
|
|
54
|
+
// 获取管理员账号并去除首尾空格
|
|
55
|
+
const account = config.adminAccount.trim();
|
|
56
|
+
// 从上下文中获取指定账号的OneBot实例
|
|
57
|
+
const bot = ctx.bots[`onebot:${account}`];
|
|
58
|
+
// 返回一个Session对象
|
|
59
|
+
return {
|
|
60
|
+
// 发送消息方法,支持队列
|
|
61
|
+
sendQueued: (content) => {
|
|
62
|
+
// 检查机器人实例是否存在
|
|
63
|
+
if (!bot) {
|
|
64
|
+
// 如果不存在,记录警告日志并显示可用的机器人实例
|
|
65
|
+
ctx.logger.warn(`未找到机器人实例: onebot:${account},当前可用: ${Object.keys(ctx.bots).join(", ") || "无"}`);
|
|
66
|
+
return Promise.resolve([]);
|
|
67
|
+
}
|
|
68
|
+
return bot.sendMessage(groupId, content);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
let checkingLoginStatus = false;
|
|
73
|
+
const checkLoginStatusProcess = async () => {
|
|
74
|
+
if (checkingLoginStatus) {
|
|
75
|
+
ctx.logger.debug("登录状态检查进行中,跳过本次定时任务");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
checkingLoginStatus = true;
|
|
79
|
+
try {
|
|
80
|
+
// ctx.logger.info("开始检测登录状态...");
|
|
81
|
+
if (!config.adminGroupID) {
|
|
82
|
+
ctx.logger.error("管理员群ID未设置,无法发送消息");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const loginStatus = await (0, login_1.checkLoginStatus)(ctx);
|
|
86
|
+
const formatter = (status) => {
|
|
87
|
+
return `微博登录状态${status ? "正常" : "异常"},当前时间戳: ${new Date().toLocaleString()}`;
|
|
88
|
+
};
|
|
89
|
+
if (!loginStatus) {
|
|
90
|
+
await sendMsgOnebot(config.adminGroupID).sendQueued(formatter(false));
|
|
91
|
+
ctx.logger.error(formatter(false));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// await sendMsgOnebot(config.adminGroupID).sendQueued(formatter(true));
|
|
95
|
+
// ctx.logger.info(formatter(true));
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
checkingLoginStatus = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const pollWeibo = (0, poll_1.createPollWeibo)(ctx, config, sendMsgOnebot);
|
|
102
|
+
// 定时任务 - 抓微博
|
|
103
|
+
ctx.setInterval(checkLoginStatusProcess, (0, timer_1.getWaitMs)(30));
|
|
104
|
+
// 定时任务 - 登录状态检测
|
|
105
|
+
ctx.setInterval(pollWeibo, (0, timer_1.getWaitMs)(config.waitMinutes));
|
|
106
|
+
if (!ctx.puppeteer.browser?.connected) {
|
|
107
|
+
await ctx.puppeteer.start();
|
|
108
|
+
}
|
|
109
|
+
(0, weibo_1.registerWeiboCommand)(ctx, config, pollWeibo);
|
|
110
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Context } from "koishi";
|
|
2
|
+
import type { TimelineEntry } from "../drawer/types";
|
|
3
|
+
/** 为时间线中的原创/转发微博批量拉取评论并写回 entry */
|
|
4
|
+
export declare const attachCommentsToEntries: (ctx: Context, entryByUID: Map<string, TimelineEntry | null>, minutes: number) => Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachCommentsToEntries = void 0;
|
|
4
|
+
const timeline_1 = require("../timeline");
|
|
5
|
+
const constants_1 = require("../../util/constants");
|
|
6
|
+
const weibo_fetch_1 = require("../weibo-fetch");
|
|
7
|
+
const filter_1 = require("./filter");
|
|
8
|
+
/** 为时间线中的原创/转发微博批量拉取评论并写回 entry */
|
|
9
|
+
const attachCommentsToEntries = async (ctx, entryByUID, minutes) => {
|
|
10
|
+
const tasks = [];
|
|
11
|
+
for (const [weiboUID, entry] of entryByUID) {
|
|
12
|
+
if (!entry?.timeline)
|
|
13
|
+
continue;
|
|
14
|
+
for (const item of entry.timeline) {
|
|
15
|
+
if (item.activityType !== timeline_1.EActivityType.like) {
|
|
16
|
+
tasks.push({ weiboUID, weiboID: String(item.id) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!tasks.length)
|
|
21
|
+
return;
|
|
22
|
+
const results = await Promise.all(tasks.map(async ({ weiboUID, weiboID }) => {
|
|
23
|
+
const result = await (0, weibo_fetch_1.getWeiboCommentsByWeiboID)(weiboID, weiboUID, ctx, {
|
|
24
|
+
count: constants_1.CONSTANTS.MAX_RENDER_COMMENTS,
|
|
25
|
+
});
|
|
26
|
+
const comments = (0, filter_1.filterCommentsWithinMinutes)(result?.comments ?? [], minutes);
|
|
27
|
+
return { weiboID, comments };
|
|
28
|
+
}));
|
|
29
|
+
const commentsByPostId = new Map(results
|
|
30
|
+
.filter((item) => item.comments.length > 0)
|
|
31
|
+
.map((item) => [item.weiboID, item.comments]));
|
|
32
|
+
for (const entry of entryByUID.values()) {
|
|
33
|
+
if (!entry?.timeline)
|
|
34
|
+
continue;
|
|
35
|
+
entry.timeline = entry.timeline.map((post) => {
|
|
36
|
+
const comments = commentsByPostId.get(String(post.id));
|
|
37
|
+
return comments?.length ? { ...post, comments } : post;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.attachCommentsToEntries = attachCommentsToEntries;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterCommentsWithinMinutes = void 0;
|
|
4
|
+
const isWithinWindow = (time, cutoff) => time != null && time >= cutoff;
|
|
5
|
+
const filterReplyTree = (comment, cutoff) => {
|
|
6
|
+
const replies = (comment.replies ?? [])
|
|
7
|
+
.map((reply) => filterReplyTree(reply, cutoff))
|
|
8
|
+
.filter((reply) => reply != null);
|
|
9
|
+
const inWindow = isWithinWindow(comment.createdAtTime, cutoff);
|
|
10
|
+
if (!inWindow && !replies.length)
|
|
11
|
+
return null;
|
|
12
|
+
return {
|
|
13
|
+
...comment,
|
|
14
|
+
replies: replies.length ? replies : undefined,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** 保留指定分钟数内的评论(含楼中楼) */
|
|
18
|
+
const filterCommentsWithinMinutes = (comments, minutes) => {
|
|
19
|
+
const windowMs = minutes > 0 ? minutes * 60 * 1000 : 60000;
|
|
20
|
+
const cutoff = Date.now() - windowMs;
|
|
21
|
+
return comments
|
|
22
|
+
.map((comment) => filterReplyTree(comment, cutoff))
|
|
23
|
+
.filter((comment) => comment != null);
|
|
24
|
+
};
|
|
25
|
+
exports.filterCommentsWithinMinutes = filterCommentsWithinMinutes;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCommentsMessages = void 0;
|
|
4
|
+
const MAX_CHUNK_LENGTH = 1500;
|
|
5
|
+
const formatCommentLines = (comment, prefix) => {
|
|
6
|
+
const name = comment.user?.screen_name ?? "匿名";
|
|
7
|
+
const author = comment.isAuthor ? "[博主] " : "";
|
|
8
|
+
const likes = comment.likesCount && comment.likesCount > 0
|
|
9
|
+
? ` (👍${comment.likesCount})`
|
|
10
|
+
: "";
|
|
11
|
+
const lines = [
|
|
12
|
+
`${prefix}[${comment.createdAtText}] ${author}${name}: ${comment.text}${likes}`,
|
|
13
|
+
];
|
|
14
|
+
comment.replies?.forEach((reply) => {
|
|
15
|
+
lines.push(...formatCommentLines(reply, " ↳ "));
|
|
16
|
+
});
|
|
17
|
+
return lines;
|
|
18
|
+
};
|
|
19
|
+
/** 将评论格式化为可读文本,并按 QQ 消息长度分段 */
|
|
20
|
+
const formatCommentsMessages = (result) => {
|
|
21
|
+
const header = `共 ${result.total} 条评论,本次 ${result.comments.length} 条:`;
|
|
22
|
+
const lines = [
|
|
23
|
+
header,
|
|
24
|
+
...result.comments.flatMap((comment, index) => formatCommentLines(comment, `${index + 1}. `)),
|
|
25
|
+
];
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let current = "";
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const next = current ? `${current}\n${line}` : line;
|
|
30
|
+
if (next.length > MAX_CHUNK_LENGTH && current) {
|
|
31
|
+
chunks.push(current);
|
|
32
|
+
current = line;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
current = next;
|
|
36
|
+
}
|
|
37
|
+
if (current) {
|
|
38
|
+
chunks.push(current);
|
|
39
|
+
}
|
|
40
|
+
return chunks;
|
|
41
|
+
};
|
|
42
|
+
exports.formatCommentsMessages = formatCommentsMessages;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeComments = void 0;
|
|
4
|
+
const constants_1 = require("../../util/constants");
|
|
5
|
+
const weibo_date_1 = require("../../util/weibo-date");
|
|
6
|
+
/** 优先 text_raw;否则从 HTML text 提取纯文本与表情 alt */
|
|
7
|
+
const extractCommentText = (comment) => {
|
|
8
|
+
if (comment?.text_raw)
|
|
9
|
+
return comment.text_raw;
|
|
10
|
+
const text = String(comment?.text || "");
|
|
11
|
+
return text
|
|
12
|
+
.replace(/<img[^>]*alt="([^"]*)"[^>]*>/gi, "$1")
|
|
13
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
14
|
+
.replace(/<[^>]+>/g, "")
|
|
15
|
+
.trim();
|
|
16
|
+
};
|
|
17
|
+
const normalizeComment = (comment, isReply = false) => {
|
|
18
|
+
if (!comment?.id)
|
|
19
|
+
return null;
|
|
20
|
+
const createdAt = comment?.created_at || "";
|
|
21
|
+
const createdAtDate = (0, weibo_date_1.parseWeiboDateString)(createdAt);
|
|
22
|
+
const replies = !isReply && Array.isArray(comment.comments) && comment.comments.length > 0
|
|
23
|
+
? comment.comments
|
|
24
|
+
.slice(0, constants_1.CONSTANTS.MAX_RENDER_COMMENT_REPLIES)
|
|
25
|
+
.map((item) => normalizeComment(item, true))
|
|
26
|
+
.filter((item) => item != null)
|
|
27
|
+
: undefined;
|
|
28
|
+
return {
|
|
29
|
+
id: String(comment.id),
|
|
30
|
+
rootId: String(comment.rootidstr ?? comment.rootid ?? comment.id),
|
|
31
|
+
text: extractCommentText(comment),
|
|
32
|
+
createdAt,
|
|
33
|
+
createdAtTime: createdAtDate?.getTime() ?? null,
|
|
34
|
+
createdAtText: createdAtDate ? (0, weibo_date_1.formatWeiboDate)(createdAtDate) : createdAt,
|
|
35
|
+
user: comment?.user,
|
|
36
|
+
likesCount: comment?.like_counts ?? comment?.like_count ?? 0,
|
|
37
|
+
source: comment?.source,
|
|
38
|
+
replyCount: Number(comment?.total_number) || replies?.length || 0,
|
|
39
|
+
isAuthor: Boolean(comment?.is_mblog_author),
|
|
40
|
+
replies: replies?.length ? replies : undefined,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const normalizeComments = (payload) => {
|
|
44
|
+
const list = Array.isArray(payload)
|
|
45
|
+
? payload
|
|
46
|
+
: Array.isArray(payload?.data)
|
|
47
|
+
? payload.data
|
|
48
|
+
: [];
|
|
49
|
+
return list
|
|
50
|
+
.map((item) => normalizeComment(item))
|
|
51
|
+
.filter((item) => item != null);
|
|
52
|
+
};
|
|
53
|
+
exports.normalizeComments = normalizeComments;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface NormalizedComment {
|
|
2
|
+
id: string;
|
|
3
|
+
/** 所属一级评论 ID;一级评论通常等于自身 id */
|
|
4
|
+
rootId: string;
|
|
5
|
+
text: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
createdAtText: string;
|
|
8
|
+
createdAtTime: number | null;
|
|
9
|
+
user?: Record<string, any>;
|
|
10
|
+
likesCount?: number;
|
|
11
|
+
source?: string;
|
|
12
|
+
/** 楼中楼总数(来自 total_number) */
|
|
13
|
+
replyCount?: number;
|
|
14
|
+
/** 是否为博主回复 */
|
|
15
|
+
isAuthor?: boolean;
|
|
16
|
+
/** 楼中楼(fetch_level=0 响应内嵌的 comments 字段) */
|
|
17
|
+
replies?: NormalizedComment[];
|
|
18
|
+
}
|
|
19
|
+
export interface WeiboCommentsResult {
|
|
20
|
+
comments: NormalizedComment[];
|
|
21
|
+
total: number;
|
|
22
|
+
maxId: string | null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProfileData, ResolvedMediaPost } from "./types";
|
|
2
|
+
export declare const buildTimelineHtml: (profile: ProfileData, normalizedTimeline: ResolvedMediaPost[]) => string;
|
|
3
|
+
export declare const buildMultiTimelineHtml: (entries: {
|
|
4
|
+
profile: ProfileData;
|
|
5
|
+
timeline: ResolvedMediaPost[];
|
|
6
|
+
}[]) => string;
|