mioku-plugin-admin 1.0.0
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/LICENSE +21 -0
- package/README.md +78 -0
- package/commands/group.ts +457 -0
- package/commands/notice.ts +60 -0
- package/commands/personal.ts +589 -0
- package/config.md +53 -0
- package/config.ts +114 -0
- package/index.ts +52 -0
- package/notify/index.ts +653 -0
- package/package.json +135 -0
- package/skills/group.ts +407 -0
- package/skills/message-image.ts +20 -0
- package/skills/personal.ts +376 -0
- package/skills.ts +7 -0
package/notify/index.ts
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { botConfig, type MiokiContext } from "mioki";
|
|
2
|
+
import type {
|
|
3
|
+
FriendRequestEvent,
|
|
4
|
+
GroupBanNoticeEvent,
|
|
5
|
+
GroupDecreaseNoticeEvent,
|
|
6
|
+
GroupInviteRequestEvent,
|
|
7
|
+
MessageEvent,
|
|
8
|
+
PrivateMessageEvent,
|
|
9
|
+
RecvAtElement,
|
|
10
|
+
RecvElement,
|
|
11
|
+
RecvFaceElement,
|
|
12
|
+
RecvFileElement,
|
|
13
|
+
RecvForwardElement,
|
|
14
|
+
RecvImageElement,
|
|
15
|
+
RecvJsonElement,
|
|
16
|
+
RecvRecordElement,
|
|
17
|
+
RecvReplyElement,
|
|
18
|
+
RecvTextElement,
|
|
19
|
+
RecvVideoElement,
|
|
20
|
+
} from "napcat-sdk";
|
|
21
|
+
import type { AdminConfig } from "../config";
|
|
22
|
+
import { formatDuration, getAvatarUrl, getGroupAvatarUrl } from "../config";
|
|
23
|
+
|
|
24
|
+
interface NotifyPayload {
|
|
25
|
+
avatarUrl?: string;
|
|
26
|
+
lines: string[];
|
|
27
|
+
rawSegments?: any[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PendingFriendRequest {
|
|
31
|
+
selfId: number;
|
|
32
|
+
userId: number;
|
|
33
|
+
flag: string;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PendingGroupInvite {
|
|
38
|
+
selfId: number;
|
|
39
|
+
groupId: number;
|
|
40
|
+
userId: number;
|
|
41
|
+
flag: string;
|
|
42
|
+
subType: string;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PENDING_TTL_MS = 24 * 60 * 60 * 1000;
|
|
47
|
+
const PENDING_MAX_SIZE = 200;
|
|
48
|
+
const EVENT_DEDUP_TTL_MS = 10 * 1000;
|
|
49
|
+
|
|
50
|
+
function normalizeErrorMessage(error: unknown): string {
|
|
51
|
+
if (error instanceof Error && error.message) return error.message;
|
|
52
|
+
if (typeof error === "string") return error;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(error);
|
|
55
|
+
} catch {
|
|
56
|
+
return String(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 获取应该通知的主人列表 */
|
|
61
|
+
function getNotifyOwners(config: AdminConfig): number[] {
|
|
62
|
+
if (config.notifyTarget.length > 0) return config.notifyTarget;
|
|
63
|
+
const owners = Array.isArray(botConfig?.owners) ? botConfig.owners : [];
|
|
64
|
+
return owners.map((v: any) => Number(v)).filter((n: number) => n > 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 注册所有事件通知处理器 */
|
|
68
|
+
export function registerNotificationHandlers(
|
|
69
|
+
ctx: MiokiContext,
|
|
70
|
+
getConfig: () => AdminConfig,
|
|
71
|
+
) {
|
|
72
|
+
const pendingFriendRequests: PendingFriendRequest[] = [];
|
|
73
|
+
const pendingGroupInvites: PendingGroupInvite[] = [];
|
|
74
|
+
const recentEventKeys = new Map<string, number>();
|
|
75
|
+
|
|
76
|
+
function owners(): number[] {
|
|
77
|
+
return getNotifyOwners(getConfig());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function prunePendingRequests() {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const isAlive = (createdAt: number) => now - createdAt <= PENDING_TTL_MS;
|
|
83
|
+
|
|
84
|
+
for (let i = pendingFriendRequests.length - 1; i >= 0; i--) {
|
|
85
|
+
if (!isAlive(pendingFriendRequests[i].createdAt)) {
|
|
86
|
+
pendingFriendRequests.splice(i, 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (let i = pendingGroupInvites.length - 1; i >= 0; i--) {
|
|
90
|
+
if (!isAlive(pendingGroupInvites[i].createdAt)) {
|
|
91
|
+
pendingGroupInvites.splice(i, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (pendingFriendRequests.length > PENDING_MAX_SIZE) {
|
|
96
|
+
pendingFriendRequests.splice(
|
|
97
|
+
0,
|
|
98
|
+
pendingFriendRequests.length - PENDING_MAX_SIZE,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (pendingGroupInvites.length > PENDING_MAX_SIZE) {
|
|
102
|
+
pendingGroupInvites.splice(
|
|
103
|
+
0,
|
|
104
|
+
pendingGroupInvites.length - PENDING_MAX_SIZE,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [key, ts] of recentEventKeys.entries()) {
|
|
109
|
+
if (now - ts > EVENT_DEDUP_TTL_MS) {
|
|
110
|
+
recentEventKeys.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function markEventOnce(key: string): boolean {
|
|
116
|
+
prunePendingRequests();
|
|
117
|
+
if (recentEventKeys.has(key)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
recentEventKeys.set(key, Date.now());
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildNotifyMessage(payload: NotifyPayload): any[] {
|
|
125
|
+
const message: any[] = [];
|
|
126
|
+
if (payload.avatarUrl) {
|
|
127
|
+
message.push(ctx.segment.image(payload.avatarUrl));
|
|
128
|
+
}
|
|
129
|
+
const text = payload.lines
|
|
130
|
+
.map((line) => String(line || "").trim())
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join("\n");
|
|
133
|
+
if (text) {
|
|
134
|
+
message.push(ctx.segment.text(text));
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(payload.rawSegments) && payload.rawSegments.length > 0) {
|
|
137
|
+
message.push(...payload.rawSegments);
|
|
138
|
+
}
|
|
139
|
+
return message.length > 0 ? message : [ctx.segment.text("")];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeIncomingSegments(segments: RecvElement[]): any[] {
|
|
143
|
+
if (!Array.isArray(segments)) return [];
|
|
144
|
+
return segments
|
|
145
|
+
.map((seg) => {
|
|
146
|
+
if (!seg || typeof seg !== "object") return null;
|
|
147
|
+
const type = String(seg.type || "");
|
|
148
|
+
|
|
149
|
+
if (!type) return null;
|
|
150
|
+
|
|
151
|
+
if (type === "text") {
|
|
152
|
+
const text = String((seg as RecvTextElement).text || "");
|
|
153
|
+
return text ? ctx.segment.text(text) : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (type === "image" || type === "record" || type === "video") {
|
|
157
|
+
const mediaSeg = seg as
|
|
158
|
+
| RecvImageElement
|
|
159
|
+
| RecvRecordElement
|
|
160
|
+
| RecvVideoElement;
|
|
161
|
+
const source = String(mediaSeg.url || mediaSeg.file || "").trim();
|
|
162
|
+
if (!source) return null;
|
|
163
|
+
if (type === "image") {
|
|
164
|
+
return ctx.segment.image(source);
|
|
165
|
+
}
|
|
166
|
+
if (type === "record") {
|
|
167
|
+
return (ctx.segment as any).record(source);
|
|
168
|
+
}
|
|
169
|
+
return (ctx.segment as any).video(source);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (type === "file") {
|
|
173
|
+
const fileSeg = seg as RecvFileElement;
|
|
174
|
+
const source = String(fileSeg.url || fileSeg.file || "").trim();
|
|
175
|
+
if (!source) return null;
|
|
176
|
+
return (ctx.segment as any).file(source);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (type === "at") {
|
|
180
|
+
const qq = (seg as RecvAtElement).qq;
|
|
181
|
+
if (qq == null) return null;
|
|
182
|
+
return ctx.segment.at(String(qq));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (type === "face") {
|
|
186
|
+
const id = (seg as RecvFaceElement).id;
|
|
187
|
+
if (id == null) return null;
|
|
188
|
+
return ctx.segment.face(Number(id));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (type === "reply") {
|
|
192
|
+
const id = (seg as RecvReplyElement).id;
|
|
193
|
+
if (id == null) return null;
|
|
194
|
+
return ctx.segment.reply(String(id));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (type === "forward") {
|
|
198
|
+
const id = (seg as RecvForwardElement).id;
|
|
199
|
+
if (id == null) return null;
|
|
200
|
+
return (ctx.segment as any).forward?.(String(id)) ?? null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (type === "json") {
|
|
204
|
+
const data = (seg as RecvJsonElement).data;
|
|
205
|
+
if (type === "json" && ctx.segment.json) {
|
|
206
|
+
return ctx.segment.json(data);
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
})
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isOwnerPrivateMessage(event: PrivateMessageEvent): boolean {
|
|
217
|
+
if (ctx.isOwner?.(event)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
const userId = Number(event.user_id || 0);
|
|
221
|
+
if (userId <= 0) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return owners().includes(userId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function sendNotify(selfId: number, payload: NotifyPayload) {
|
|
228
|
+
const bot = ctx.pickBot(selfId);
|
|
229
|
+
if (!bot) return;
|
|
230
|
+
for (const ownerId of owners()) {
|
|
231
|
+
try {
|
|
232
|
+
await bot.sendPrivateMsg(ownerId, buildNotifyMessage(payload));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
ctx.logger.error(`admin notify owner ${ownerId} failed: ${err}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function pushPendingFriendRequest(event: FriendRequestEvent) {
|
|
240
|
+
const selfId = Number(event.self_id || 0);
|
|
241
|
+
const userId = Number(event.user_id || 0);
|
|
242
|
+
const flag = String(event.flag || "").trim();
|
|
243
|
+
if (!selfId || !userId || !flag) return;
|
|
244
|
+
pendingFriendRequests.push({
|
|
245
|
+
selfId,
|
|
246
|
+
userId,
|
|
247
|
+
flag,
|
|
248
|
+
createdAt: Date.now(),
|
|
249
|
+
});
|
|
250
|
+
prunePendingRequests();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function pushPendingGroupInvite(event: GroupInviteRequestEvent) {
|
|
254
|
+
const selfId = Number(event.self_id || 0);
|
|
255
|
+
const groupId = Number(event.group_id || 0);
|
|
256
|
+
const userId = Number(event.user_id || 0);
|
|
257
|
+
const flag = String(event.flag || "").trim();
|
|
258
|
+
const subType = String(event.sub_type || "invite").trim() || "invite";
|
|
259
|
+
if (!selfId || !groupId || !userId || !flag) return;
|
|
260
|
+
pendingGroupInvites.push({
|
|
261
|
+
selfId,
|
|
262
|
+
groupId,
|
|
263
|
+
userId,
|
|
264
|
+
flag,
|
|
265
|
+
subType,
|
|
266
|
+
createdAt: Date.now(),
|
|
267
|
+
});
|
|
268
|
+
prunePendingRequests();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function shiftLatestFriendRequest(
|
|
272
|
+
selfId: number,
|
|
273
|
+
userId: number,
|
|
274
|
+
): PendingFriendRequest | undefined {
|
|
275
|
+
prunePendingRequests();
|
|
276
|
+
for (let i = pendingFriendRequests.length - 1; i >= 0; i--) {
|
|
277
|
+
const item = pendingFriendRequests[i];
|
|
278
|
+
if (item.selfId === selfId && item.userId === userId) {
|
|
279
|
+
pendingFriendRequests.splice(i, 1);
|
|
280
|
+
return item;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function shiftLatestGroupInvite(
|
|
287
|
+
selfId: number,
|
|
288
|
+
groupId: number,
|
|
289
|
+
userId?: number,
|
|
290
|
+
): PendingGroupInvite | undefined {
|
|
291
|
+
prunePendingRequests();
|
|
292
|
+
for (let i = pendingGroupInvites.length - 1; i >= 0; i--) {
|
|
293
|
+
const item = pendingGroupInvites[i];
|
|
294
|
+
if (item.selfId !== selfId || item.groupId !== groupId) continue;
|
|
295
|
+
if (userId && item.userId !== userId) continue;
|
|
296
|
+
pendingGroupInvites.splice(i, 1);
|
|
297
|
+
return item;
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractTextFromSegments(segments: RecvElement[]): string {
|
|
303
|
+
if (!Array.isArray(segments)) return "";
|
|
304
|
+
return segments
|
|
305
|
+
.map((seg) => {
|
|
306
|
+
if (seg.type !== "text") return "";
|
|
307
|
+
return String((seg as RecvTextElement).text || "");
|
|
308
|
+
})
|
|
309
|
+
.join("")
|
|
310
|
+
.trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function resolveQuotedText(event: MessageEvent): Promise<string> {
|
|
314
|
+
if (!event.quote_id) return "";
|
|
315
|
+
try {
|
|
316
|
+
const quoted = await event.getQuoteMsg();
|
|
317
|
+
return extractTextFromSegments(quoted?.message || []);
|
|
318
|
+
} catch {
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseQuotedApprovalTarget(
|
|
324
|
+
quotedText: string,
|
|
325
|
+
):
|
|
326
|
+
| { type: "friend_message"; userId: number }
|
|
327
|
+
| { type: "friend_request"; userId: number }
|
|
328
|
+
| { type: "group_invite"; groupId: number; userId?: number }
|
|
329
|
+
| { type: "group_ban"; groupId: number }
|
|
330
|
+
| null {
|
|
331
|
+
const text = String(quotedText || "");
|
|
332
|
+
if (!text) return null;
|
|
333
|
+
|
|
334
|
+
if (text.includes("[好友消息]")) {
|
|
335
|
+
const userIdMatch =
|
|
336
|
+
text.match(/好友QQ[::]\s*(\d+)/) ||
|
|
337
|
+
text.match(/QQ[::]\s*(\d+)/);
|
|
338
|
+
const userId = Number(userIdMatch?.[1] || 0);
|
|
339
|
+
return userId > 0 ? { type: "friend_message", userId } : null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (text.includes("[好友申请]")) {
|
|
343
|
+
const userIdMatch =
|
|
344
|
+
text.match(/好友QQ[::]\s*(\d+)/) ||
|
|
345
|
+
text.match(/QQ[::]\s*(\d+)/) ||
|
|
346
|
+
text.match(/\[好友申请\]\s*(\d+)/);
|
|
347
|
+
const userId = Number(userIdMatch?.[1] || 0);
|
|
348
|
+
return userId > 0 ? { type: "friend_request", userId } : null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (text.includes("[群邀请]")) {
|
|
352
|
+
const groupIdMatch =
|
|
353
|
+
text.match(/群号[::]\s*(\d+)/) || text.match(/\[群邀请\][^\d]*(\d+)/);
|
|
354
|
+
const inviterIdMatch = text.match(/邀请人QQ[::]\s*(\d+)/);
|
|
355
|
+
const groupId = Number(groupIdMatch?.[1] || 0);
|
|
356
|
+
const userId = Number(inviterIdMatch?.[1] || 0);
|
|
357
|
+
if (groupId <= 0) return null;
|
|
358
|
+
return userId > 0
|
|
359
|
+
? { type: "group_invite", groupId, userId }
|
|
360
|
+
: { type: "group_invite", groupId };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (text.includes("[Bot被禁言]")) {
|
|
364
|
+
const groupIdMatch = text.match(/群号[::]\s*(\d+)/);
|
|
365
|
+
const groupId = Number(groupIdMatch?.[1] || 0);
|
|
366
|
+
return groupId > 0 ? { type: "group_ban", groupId } : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function extractReplyPayloadSegments(event: MessageEvent): any[] {
|
|
373
|
+
return normalizeIncomingSegments(event?.message || []).filter(
|
|
374
|
+
(seg: any) => {
|
|
375
|
+
const type = String(seg?.type || "");
|
|
376
|
+
return type !== "reply";
|
|
377
|
+
},
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isApproveText(text: string): boolean {
|
|
382
|
+
const normalized = String(text || "").trim();
|
|
383
|
+
return normalized === "通过" || normalized === "同意";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isRejectText(text: string): boolean {
|
|
387
|
+
const normalized = String(text || "").trim();
|
|
388
|
+
return normalized === "拒绝" || normalized === "驳回";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function notifyGroupInvite(
|
|
392
|
+
event: GroupInviteRequestEvent,
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
if (!getConfig().notifyGroupInvite) return;
|
|
395
|
+
|
|
396
|
+
const selfId = Number(event.self_id || 0);
|
|
397
|
+
const groupId = Number(event.group_id || 0);
|
|
398
|
+
const userId = Number(event.user_id || 0);
|
|
399
|
+
const flag = String(event.flag || "").trim();
|
|
400
|
+
if (!selfId || !groupId || !userId || !flag) return;
|
|
401
|
+
|
|
402
|
+
const eventKey = `group-invite:${selfId}:${groupId}:${userId}:${flag}`;
|
|
403
|
+
if (!markEventOnce(eventKey)) return;
|
|
404
|
+
|
|
405
|
+
pushPendingGroupInvite(event);
|
|
406
|
+
|
|
407
|
+
const comment = event.comment || "无";
|
|
408
|
+
await sendNotify(selfId, {
|
|
409
|
+
avatarUrl: getGroupAvatarUrl(groupId),
|
|
410
|
+
lines: [
|
|
411
|
+
"[群邀请]",
|
|
412
|
+
`群号:${groupId}`,
|
|
413
|
+
`邀请人QQ:${userId}`,
|
|
414
|
+
`验证消息:${comment}`,
|
|
415
|
+
"备注:引用该消息回复「同意」或「拒绝」",
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function notifyGroupBan(event: GroupBanNoticeEvent): Promise<void> {
|
|
421
|
+
const selfId = Number(event.self_id || 0);
|
|
422
|
+
const groupId = Number(event.group_id || 0);
|
|
423
|
+
const userId = Number(event.user_id || 0);
|
|
424
|
+
const duration = Number(event.duration || 0);
|
|
425
|
+
if (!selfId || !groupId) return;
|
|
426
|
+
if (userId !== selfId) return;
|
|
427
|
+
|
|
428
|
+
const operatorId = Number(event.operator_id || 0);
|
|
429
|
+
const isUnban = event.action_type === "lift_ban";
|
|
430
|
+
if (isUnban) {
|
|
431
|
+
if (!getConfig().notifyGroupUnban) return;
|
|
432
|
+
} else if (!getConfig().notifyGroupBan) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const eventKey = `group-ban:${selfId}:${groupId}:${operatorId}:${duration}:${event.action_type}:${Number(event.time || 0)}`;
|
|
437
|
+
if (!markEventOnce(eventKey)) return;
|
|
438
|
+
|
|
439
|
+
await sendNotify(selfId, {
|
|
440
|
+
avatarUrl: getGroupAvatarUrl(groupId),
|
|
441
|
+
lines: [
|
|
442
|
+
isUnban ? "[Bot被解除禁言]" : "[Bot被禁言]",
|
|
443
|
+
`群号:${groupId}`,
|
|
444
|
+
`操作人QQ:${operatorId}`,
|
|
445
|
+
isUnban ? "" : `禁言时长:${formatDuration(duration)}`,
|
|
446
|
+
isUnban ? "" : "引用该消息回复「退群」可退出该群",
|
|
447
|
+
],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function notifyGroupKick(
|
|
452
|
+
event: GroupDecreaseNoticeEvent,
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
if (!getConfig().notifyGroupKick) return;
|
|
455
|
+
|
|
456
|
+
const selfId = Number(event.self_id || 0);
|
|
457
|
+
const groupId = Number(event.group_id || 0);
|
|
458
|
+
const userId = Number(event.user_id || 0);
|
|
459
|
+
const leaveType = String((event as any).action_type || "").trim();
|
|
460
|
+
if (!selfId || !groupId) return;
|
|
461
|
+
if (userId !== selfId) return;
|
|
462
|
+
if (leaveType !== "kick" && leaveType !== "kick_me") return;
|
|
463
|
+
|
|
464
|
+
const operatorId = Number(event.operator_id || 0);
|
|
465
|
+
const eventKey = `group-kick:${selfId}:${groupId}:${operatorId}:${leaveType}:${Number(event.time || 0)}`;
|
|
466
|
+
if (!markEventOnce(eventKey)) return;
|
|
467
|
+
|
|
468
|
+
await sendNotify(selfId, {
|
|
469
|
+
avatarUrl:
|
|
470
|
+
operatorId > 0 ? getAvatarUrl(operatorId) : getGroupAvatarUrl(groupId),
|
|
471
|
+
lines: ["[Bot被踢]", `群号:${groupId}`, `操作者QQ:${operatorId}`],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 好友私聊消息通知
|
|
476
|
+
ctx.handle("message.private", async (event: PrivateMessageEvent) => {
|
|
477
|
+
if (!getConfig().notifyFriendMsg) return;
|
|
478
|
+
if (event.user_id === event.self_id) return;
|
|
479
|
+
if (isOwnerPrivateMessage(event)) return;
|
|
480
|
+
|
|
481
|
+
const userId = event.user_id;
|
|
482
|
+
const nickname = event.sender?.nickname || String(userId);
|
|
483
|
+
const rawSegments = normalizeIncomingSegments(event.message || []);
|
|
484
|
+
|
|
485
|
+
await sendNotify(event.self_id, {
|
|
486
|
+
avatarUrl: getAvatarUrl(userId),
|
|
487
|
+
lines: [
|
|
488
|
+
"[好友消息]",
|
|
489
|
+
`好友昵称:${nickname}`,
|
|
490
|
+
`好友QQ:${userId}`,
|
|
491
|
+
"消息:",
|
|
492
|
+
"引用该消息回复",
|
|
493
|
+
],
|
|
494
|
+
rawSegments,
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// 好友申请通知
|
|
499
|
+
ctx.handle("request.friend", async (event: FriendRequestEvent) => {
|
|
500
|
+
pushPendingFriendRequest(event);
|
|
501
|
+
if (!getConfig().notifyFriendRequest) return;
|
|
502
|
+
|
|
503
|
+
const userId = event.user_id;
|
|
504
|
+
const comment = event.comment || "无";
|
|
505
|
+
|
|
506
|
+
await sendNotify(event.self_id, {
|
|
507
|
+
avatarUrl: getAvatarUrl(userId),
|
|
508
|
+
lines: [
|
|
509
|
+
"[好友申请]",
|
|
510
|
+
`好友QQ:${userId}`,
|
|
511
|
+
`验证消息:${comment}`,
|
|
512
|
+
"引用该消息回复「同意」或「拒绝」",
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// 群邀请通知
|
|
518
|
+
ctx.handle("request.group.invite", async (event: GroupInviteRequestEvent) => {
|
|
519
|
+
await notifyGroupInvite(event);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Bot被禁言通知
|
|
523
|
+
ctx.handle("notice.group.ban", async (event: GroupBanNoticeEvent) => {
|
|
524
|
+
await notifyGroupBan(event);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Bot被踢通知
|
|
528
|
+
ctx.handle(
|
|
529
|
+
"notice.group.decrease",
|
|
530
|
+
async (event: GroupDecreaseNoticeEvent) => {
|
|
531
|
+
await notifyGroupKick(event);
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// 引用回复处理
|
|
536
|
+
ctx.handle("message", async (event: MessageEvent) => {
|
|
537
|
+
if (event.message_type !== "private") return;
|
|
538
|
+
if (!ctx.isOwner?.(event)) return;
|
|
539
|
+
|
|
540
|
+
const quotedText = await resolveQuotedText(event);
|
|
541
|
+
if (!quotedText) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const target = parseQuotedApprovalTarget(quotedText);
|
|
546
|
+
if (!target) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const text = (ctx.text(event) || "").trim();
|
|
550
|
+
|
|
551
|
+
const selfId = Number(event.self_id || 0);
|
|
552
|
+
const bot = ctx.pickBot(selfId);
|
|
553
|
+
if (!bot) {
|
|
554
|
+
await event.reply("Bot不可用", true);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (target.type === "friend_message") {
|
|
559
|
+
const payload = extractReplyPayloadSegments(event);
|
|
560
|
+
if (!payload.length) {
|
|
561
|
+
await event.reply("回复内容不能为空", true);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
await bot.sendPrivateMsg(target.userId, payload);
|
|
566
|
+
await event.reply("done");
|
|
567
|
+
} catch (err) {
|
|
568
|
+
ctx.logger.error(
|
|
569
|
+
`[admin notify] 回发好友消息失败: ${normalizeErrorMessage(err)}`,
|
|
570
|
+
);
|
|
571
|
+
await event.reply(`出错了,笨蛋~ ${String(err)}`, true);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (target.type === "friend_request") {
|
|
577
|
+
if (!isApproveText(text) && !isRejectText(text)) {
|
|
578
|
+
await event.reply("请引用该消息回复「同意」或「拒绝」", true);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const pending = shiftLatestFriendRequest(selfId, target.userId);
|
|
582
|
+
if (!pending) {
|
|
583
|
+
await event.reply("没找到待处理的好友申请", true);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
await bot.api("set_friend_add_request", {
|
|
589
|
+
flag: pending.flag,
|
|
590
|
+
approve: isApproveText(text),
|
|
591
|
+
});
|
|
592
|
+
await event.reply("done");
|
|
593
|
+
} catch (err) {
|
|
594
|
+
ctx.logger.error(
|
|
595
|
+
`[admin notify] 处理好友申请失败: ${normalizeErrorMessage(err)}`,
|
|
596
|
+
);
|
|
597
|
+
await event.reply(`出错了,笨蛋~ ${String(err)}`, true);
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (target.type === "group_ban") {
|
|
603
|
+
if (text !== "退群") {
|
|
604
|
+
await event.reply("请引用该消息回复「退群」", true);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
await bot.api("set_group_leave", {
|
|
609
|
+
group_id: target.groupId,
|
|
610
|
+
is_dismiss: false,
|
|
611
|
+
});
|
|
612
|
+
await event.reply("done");
|
|
613
|
+
} catch (err) {
|
|
614
|
+
ctx.logger.error(
|
|
615
|
+
`[admin notify] 引用退群失败: ${normalizeErrorMessage(err)}`,
|
|
616
|
+
);
|
|
617
|
+
await event.reply(`出错了,笨蛋~ ${String(err)}`, true);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (target.type === "group_invite") {
|
|
623
|
+
if (!isApproveText(text) && !isRejectText(text)) {
|
|
624
|
+
await event.reply("请引用该消息回复「同意」或「拒绝」", true);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const pending = shiftLatestGroupInvite(
|
|
629
|
+
selfId,
|
|
630
|
+
target.groupId,
|
|
631
|
+
target.userId,
|
|
632
|
+
);
|
|
633
|
+
if (!pending) {
|
|
634
|
+
await event.reply("没找到待处理的群邀请", true);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
await bot.api("set_group_add_request", {
|
|
640
|
+
flag: pending.flag,
|
|
641
|
+
sub_type: pending.subType || "invite",
|
|
642
|
+
approve: isApproveText(text),
|
|
643
|
+
});
|
|
644
|
+
await event.reply("done");
|
|
645
|
+
} catch (err) {
|
|
646
|
+
ctx.logger.error(
|
|
647
|
+
`[admin notify] 处理群邀请失败: ${normalizeErrorMessage(err)}`,
|
|
648
|
+
);
|
|
649
|
+
await event.reply(`出错了,笨蛋~ ${String(err)}`, true);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|