koishi-plugin-group-control 0.2.7 → 0.2.9
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/lib/config.d.ts +7 -2
- package/lib/database.d.ts +1 -1
- package/lib/index.js +260 -173
- package/lib/utils.d.ts +9 -0
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 muyni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/lib/config.d.ts
CHANGED
|
@@ -5,13 +5,13 @@ export interface GroupConfig {
|
|
|
5
5
|
quitMessage: string;
|
|
6
6
|
enableBlacklist: boolean;
|
|
7
7
|
quitCommandEnabled: boolean;
|
|
8
|
-
quitCommandAuthority: number;
|
|
9
8
|
notifyAdminOnKick: boolean;
|
|
10
9
|
kickNotificationMessage: string;
|
|
11
10
|
smallGroupAutoQuit: boolean;
|
|
12
11
|
smallGroupThreshold: number;
|
|
13
12
|
smallGroupQuitMessage: string;
|
|
14
13
|
smallGroupNotifyAdmin: boolean;
|
|
14
|
+
smallGroupCheckDelay: number;
|
|
15
15
|
}
|
|
16
16
|
export interface GroupInviteConfig {
|
|
17
17
|
enabled: boolean;
|
|
@@ -37,12 +37,17 @@ export interface BotSwitchConfig {
|
|
|
37
37
|
enabled: boolean;
|
|
38
38
|
defaultState: boolean;
|
|
39
39
|
disabledMessage: string;
|
|
40
|
-
|
|
40
|
+
}
|
|
41
|
+
export interface PermissionConfig {
|
|
42
|
+
mode: 'koishi' | 'builtin';
|
|
43
|
+
koishiAuthority: number;
|
|
44
|
+
protectedCommands: string[];
|
|
41
45
|
}
|
|
42
46
|
export interface Config {
|
|
43
47
|
basic: GroupConfig;
|
|
44
48
|
frequency: FrequencyConfig;
|
|
45
49
|
invite: GroupInviteConfig;
|
|
46
50
|
botSwitch: BotSwitchConfig;
|
|
51
|
+
permission: PermissionConfig;
|
|
47
52
|
}
|
|
48
53
|
export declare const Config: Schema<Config>;
|
package/lib/database.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function apply(ctx: Context): void;
|
|
|
31
31
|
export declare const BLACKLIST_PLATFORM = "onebot";
|
|
32
32
|
export declare function getBlacklistedGuild(ctx: Context, guildId: string): Promise<BlacklistedGuild[]>;
|
|
33
33
|
export declare function removeBlacklistedGuild(ctx: Context, guildId: string): Promise<import("minato").Driver.WriteResult>;
|
|
34
|
-
export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<
|
|
34
|
+
export declare function createBlacklistedGuild(ctx: Context, guildId: string, reason: string): Promise<import("minato").Driver.WriteResult>;
|
|
35
35
|
export declare function getAllBlacklistedGuilds(ctx: Context): Promise<BlacklistedGuild[]>;
|
|
36
36
|
export declare function clearBlacklistedGuilds(ctx: Context): Promise<import("minato").Driver.WriteResult>;
|
|
37
37
|
export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
|
package/lib/index.js
CHANGED
|
@@ -76,12 +76,12 @@ async function removeBlacklistedGuild(ctx, guildId) {
|
|
|
76
76
|
}
|
|
77
77
|
__name(removeBlacklistedGuild, "removeBlacklistedGuild");
|
|
78
78
|
async function createBlacklistedGuild(ctx, guildId, reason) {
|
|
79
|
-
return await ctx.model.
|
|
79
|
+
return await ctx.model.upsert("blacklisted_guild", [{
|
|
80
80
|
platform: BLACKLIST_PLATFORM,
|
|
81
81
|
guildId,
|
|
82
82
|
timestamp: Math.floor(Date.now() / 1e3),
|
|
83
83
|
reason
|
|
84
|
-
});
|
|
84
|
+
}]);
|
|
85
85
|
}
|
|
86
86
|
__name(createBlacklistedGuild, "createBlacklistedGuild");
|
|
87
87
|
async function getAllBlacklistedGuilds(ctx) {
|
|
@@ -157,11 +157,76 @@ async function notifyAdmins(bot, config, message) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
__name(notifyAdmins, "notifyAdmins");
|
|
160
|
+
async function hasPermission(session, config) {
|
|
161
|
+
if (config.permission.mode === "koishi") {
|
|
162
|
+
try {
|
|
163
|
+
const user = session.user;
|
|
164
|
+
if (user && typeof user.authority === "number") {
|
|
165
|
+
return user.authority >= config.permission.koishiAuthority;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const userId = session.userId;
|
|
172
|
+
if (config.invite.adminQQs?.includes(userId)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const member = await session.bot.getGuildMember(session.guildId, userId);
|
|
177
|
+
const roles = member?.roles || member?.role;
|
|
178
|
+
if (roles) {
|
|
179
|
+
if (Array.isArray(roles)) {
|
|
180
|
+
return roles.some((r) => r === "admin" || r === "owner");
|
|
181
|
+
}
|
|
182
|
+
return roles === "admin" || roles === "owner" || roles === "administrator";
|
|
183
|
+
}
|
|
184
|
+
const role = member?.role;
|
|
185
|
+
if (role === "admin" || role === "owner") return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
try {
|
|
188
|
+
const info = await session.bot.internal?.getGroupMemberInfo?.(
|
|
189
|
+
parseInt(session.guildId),
|
|
190
|
+
parseInt(userId)
|
|
191
|
+
);
|
|
192
|
+
if (info) {
|
|
193
|
+
return info.role === "admin" || info.role === "owner";
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
__name(hasPermission, "hasPermission");
|
|
201
|
+
var ADMIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
202
|
+
"bot-on",
|
|
203
|
+
"bot-off",
|
|
204
|
+
"quit",
|
|
205
|
+
"view-blacklist",
|
|
206
|
+
"remove-from-blacklist",
|
|
207
|
+
"add-to-blacklist",
|
|
208
|
+
"clear-blacklist",
|
|
209
|
+
"approve",
|
|
210
|
+
"reject",
|
|
211
|
+
"pending-invites"
|
|
212
|
+
]);
|
|
160
213
|
|
|
161
214
|
// src/modules/basic.ts
|
|
162
215
|
var name2 = "group-control-basic";
|
|
163
216
|
function apply2(ctx, config) {
|
|
164
|
-
const quittingGuilds = /* @__PURE__ */ new
|
|
217
|
+
const quittingGuilds = /* @__PURE__ */ new Map();
|
|
218
|
+
const processedKicks = /* @__PURE__ */ new Map();
|
|
219
|
+
const QUITTING_EXPIRE_MS = 60 * 1e3;
|
|
220
|
+
const KICK_DEDUP_MS = 60 * 1e3;
|
|
221
|
+
setInterval(() => {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
for (const [key, time] of quittingGuilds) {
|
|
224
|
+
if (now - time > QUITTING_EXPIRE_MS) quittingGuilds.delete(key);
|
|
225
|
+
}
|
|
226
|
+
for (const [key, time] of processedKicks) {
|
|
227
|
+
if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
|
|
228
|
+
}
|
|
229
|
+
}, 30 * 1e3);
|
|
165
230
|
ctx.on("guild-added", async (session) => {
|
|
166
231
|
const { guildId, platform } = session;
|
|
167
232
|
if (config.basic.enableBlacklist) {
|
|
@@ -171,7 +236,7 @@ function apply2(ctx, config) {
|
|
|
171
236
|
await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
|
|
172
237
|
} catch (e) {
|
|
173
238
|
}
|
|
174
|
-
quittingGuilds.
|
|
239
|
+
quittingGuilds.set(`${platform}:${guildId}`, Date.now());
|
|
175
240
|
try {
|
|
176
241
|
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
177
242
|
} catch (e) {
|
|
@@ -180,34 +245,54 @@ function apply2(ctx, config) {
|
|
|
180
245
|
}
|
|
181
246
|
}
|
|
182
247
|
if (config.basic.smallGroupAutoQuit) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const quitMsg = config.basic.smallGroupQuitMessage.replace("{memberCount}", memberCount.toString()).replace("{threshold}", config.basic.smallGroupThreshold.toString());
|
|
248
|
+
const delay = config.basic.smallGroupCheckDelay || 3e3;
|
|
249
|
+
setTimeout(async () => {
|
|
250
|
+
try {
|
|
251
|
+
let memberCount = 0;
|
|
188
252
|
try {
|
|
189
|
-
await session.bot.
|
|
190
|
-
|
|
253
|
+
const groupInfo = await session.bot.internal?.getGroupInfo?.(parseInt(guildId));
|
|
254
|
+
memberCount = groupInfo?.member_count || 0;
|
|
255
|
+
} catch {
|
|
191
256
|
}
|
|
192
|
-
if (
|
|
193
|
-
|
|
257
|
+
if (memberCount === 0) {
|
|
258
|
+
try {
|
|
259
|
+
const guildInfo = await session.bot.getGuild(guildId);
|
|
260
|
+
memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (memberCount === 0) {
|
|
265
|
+
try {
|
|
266
|
+
const memberList = await session.bot.getGuildMemberList(guildId);
|
|
267
|
+
memberCount = memberList?.data?.length || 0;
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
|
|
272
|
+
const quitMsg = config.basic.smallGroupQuitMessage.replace("{memberCount}", memberCount.toString()).replace("{threshold}", config.basic.smallGroupThreshold.toString());
|
|
273
|
+
try {
|
|
274
|
+
await session.bot.sendMessage(guildId, quitMsg, platform);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
}
|
|
277
|
+
if (config.basic.smallGroupNotifyAdmin) {
|
|
278
|
+
const adminMsg = `小群自动退群
|
|
194
279
|
群号:${guildId}
|
|
195
280
|
群成员数:${memberCount}人(阈值:${config.basic.smallGroupThreshold}人)
|
|
196
281
|
机器人已自动退出该群。`;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
282
|
+
await notifyAdmins(session.bot, config, adminMsg);
|
|
283
|
+
}
|
|
284
|
+
quittingGuilds.set(`${platform}:${guildId}`, Date.now());
|
|
285
|
+
try {
|
|
286
|
+
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
|
|
289
|
+
quittingGuilds.delete(`${platform}:${guildId}`);
|
|
290
|
+
}
|
|
205
291
|
}
|
|
206
|
-
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(`小群自动退群检测失败 (群号: ${guildId}):`, error);
|
|
207
294
|
}
|
|
208
|
-
}
|
|
209
|
-
console.error(`获取群信息失败 (群号: ${guildId}):`, error);
|
|
210
|
-
}
|
|
295
|
+
}, delay);
|
|
211
296
|
}
|
|
212
297
|
if (config.basic.welcomeMessage) {
|
|
213
298
|
try {
|
|
@@ -220,9 +305,12 @@ function apply2(ctx, config) {
|
|
|
220
305
|
const { guildId, platform } = session;
|
|
221
306
|
const quittingKey = `${platform}:${guildId}`;
|
|
222
307
|
if (quittingGuilds.has(quittingKey)) {
|
|
223
|
-
quittingGuilds.delete(quittingKey);
|
|
224
308
|
return;
|
|
225
309
|
}
|
|
310
|
+
if (processedKicks.has(quittingKey)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
processedKicks.set(quittingKey, Date.now());
|
|
226
314
|
if (config.basic.enableBlacklist) {
|
|
227
315
|
await ctx.model.upsert("blacklisted_guild", [{
|
|
228
316
|
platform,
|
|
@@ -237,10 +325,21 @@ function apply2(ctx, config) {
|
|
|
237
325
|
}
|
|
238
326
|
});
|
|
239
327
|
if (config.basic.quitCommandEnabled) {
|
|
240
|
-
|
|
328
|
+
const cmdOpts = {};
|
|
329
|
+
if (config.permission.mode === "koishi") {
|
|
330
|
+
cmdOpts.authority = config.permission.koishiAuthority;
|
|
331
|
+
}
|
|
332
|
+
ctx.command("quit", "让机器人主动退出当前群聊", cmdOpts).action(async ({ session }) => {
|
|
241
333
|
if (!session.guildId) return "quit 指令只能在群聊中使用。";
|
|
334
|
+
if (config.permission.mode === "builtin") {
|
|
335
|
+
const hasPerm = await hasPermission(session, config);
|
|
336
|
+
if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
|
|
337
|
+
}
|
|
242
338
|
const { guildId, platform, userId } = session;
|
|
243
|
-
|
|
339
|
+
const adminMsg = `收到来自 ${userId} 的退群指令
|
|
340
|
+
群号:${guildId}`;
|
|
341
|
+
await notifyAdmins(session.bot, config, adminMsg);
|
|
342
|
+
quittingGuilds.set(`${platform}:${guildId}`, Date.now());
|
|
244
343
|
try {
|
|
245
344
|
await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replace("{userId}", userId), platform);
|
|
246
345
|
} catch (e) {
|
|
@@ -267,6 +366,18 @@ var name3 = "group-control-invite";
|
|
|
267
366
|
function apply3(ctx, config) {
|
|
268
367
|
if (!config.invite.enabled) return;
|
|
269
368
|
const pendingInvites = /* @__PURE__ */ new Map();
|
|
369
|
+
const INVITE_TIMEOUT = 10 * 60 * 1e3;
|
|
370
|
+
setInterval(() => {
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
for (const [key, invite] of pendingInvites) {
|
|
373
|
+
if (now - invite.time > INVITE_TIMEOUT) {
|
|
374
|
+
pendingInvites.delete(key);
|
|
375
|
+
if (config.invite.showDetailedLog) {
|
|
376
|
+
console.log(`邀请超时已清理: 群号=${invite.groupId}, 邀请者=${invite.userId}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}, 60 * 1e3);
|
|
270
381
|
ctx.on("guild-request", async (session) => {
|
|
271
382
|
const raw = session.original || session.raw || session.event?._data || {};
|
|
272
383
|
const flag = raw.flag || session.flag || session.messageId;
|
|
@@ -294,7 +405,7 @@ function apply3(ctx, config) {
|
|
|
294
405
|
console.error("获取群信息失败:", error);
|
|
295
406
|
}
|
|
296
407
|
try {
|
|
297
|
-
const waitMessage = config.invite.inviteWaitMessage.
|
|
408
|
+
const waitMessage = config.invite.inviteWaitMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", rawGroupId).replaceAll("{userName}", userName).replaceAll("{userId}", rawUserId);
|
|
298
409
|
await session.bot.sendPrivateMessage(rawUserId, waitMessage);
|
|
299
410
|
} catch (error) {
|
|
300
411
|
console.error(`发送等待审核提示给 ${rawUserId} 失败:`, error);
|
|
@@ -302,12 +413,7 @@ function apply3(ctx, config) {
|
|
|
302
413
|
if (!config.invite.adminQQs || config.invite.adminQQs.length === 0) {
|
|
303
414
|
if (config.invite.autoApprove) {
|
|
304
415
|
try {
|
|
305
|
-
await session.bot.internal.setGroupAddRequest(
|
|
306
|
-
flag,
|
|
307
|
-
sub_type: "invite",
|
|
308
|
-
approve: true,
|
|
309
|
-
reason: ""
|
|
310
|
-
});
|
|
416
|
+
await session.bot.internal.setGroupAddRequest(flag, "invite", true, "");
|
|
311
417
|
if (config.invite.showDetailedLog) {
|
|
312
418
|
console.log(`自动同意群聊邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
|
|
313
419
|
}
|
|
@@ -317,15 +423,15 @@ function apply3(ctx, config) {
|
|
|
317
423
|
}
|
|
318
424
|
return;
|
|
319
425
|
}
|
|
320
|
-
|
|
321
|
-
pendingInvites.set(inviteId, {
|
|
426
|
+
pendingInvites.set(rawGroupId, {
|
|
322
427
|
groupId: rawGroupId,
|
|
323
428
|
userId: rawUserId,
|
|
324
429
|
userName,
|
|
430
|
+
groupName,
|
|
325
431
|
time: Date.now(),
|
|
326
432
|
flag
|
|
327
433
|
});
|
|
328
|
-
const requestMessage = config.invite.inviteRequestMessage.
|
|
434
|
+
const requestMessage = config.invite.inviteRequestMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", rawGroupId).replaceAll("{userName}", userName).replaceAll("{userId}", rawUserId);
|
|
329
435
|
let requestSent = false;
|
|
330
436
|
if (config.invite.notificationGroupId) {
|
|
331
437
|
try {
|
|
@@ -355,117 +461,69 @@ function apply3(ctx, config) {
|
|
|
355
461
|
console.warn("群邀请请求发送失败:未配置通知群且管理员私聊发送失败");
|
|
356
462
|
}
|
|
357
463
|
});
|
|
358
|
-
ctx.
|
|
359
|
-
|
|
360
|
-
if (!config.invite.adminQQs.includes(userId))
|
|
361
|
-
|
|
362
|
-
const isPrivate = !guildId;
|
|
363
|
-
if (!isNotificationGroup && !isPrivate && config.invite.notificationGroupId) return;
|
|
364
|
-
const hasQuote = session.elements.some((element) => element.type === "quote");
|
|
365
|
-
if (!hasQuote) return;
|
|
366
|
-
const textContent = session.elements.filter((element) => element.type === "text").map((element) => element.attrs?.content || "").join("").trim();
|
|
367
|
-
if (config.invite.showDetailedLog) {
|
|
368
|
-
console.log(`管理员审核回复 - 原始content: "${session.content}", 提取文本: "${textContent}"`);
|
|
464
|
+
ctx.command("approve <groupId:string>", "同意群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
|
|
465
|
+
if (!groupId) return "请指定群号。用法:approve <群号>";
|
|
466
|
+
if (!config.invite.adminQQs.includes(session.userId)) {
|
|
467
|
+
return "权限不足,只有管理员可以审核邀请。";
|
|
369
468
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let quoteMessageContent = "";
|
|
374
|
-
if (session.quote?.content) {
|
|
375
|
-
quoteMessageContent = session.quote.content;
|
|
469
|
+
const inviteData = pendingInvites.get(groupId);
|
|
470
|
+
if (!inviteData) {
|
|
471
|
+
return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
|
|
376
472
|
}
|
|
377
|
-
|
|
378
|
-
|
|
473
|
+
try {
|
|
474
|
+
await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", true, "");
|
|
475
|
+
try {
|
|
476
|
+
await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("通知邀请者失败:", error);
|
|
479
|
+
}
|
|
480
|
+
pendingInvites.delete(groupId);
|
|
481
|
+
return `已同意加入群 ${groupId}(${inviteData.groupName}),邀请者:${inviteData.userName}`;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error("处理同意邀请失败:", error);
|
|
484
|
+
return `处理同意邀请失败: ${error.message}`;
|
|
379
485
|
}
|
|
380
|
-
|
|
381
|
-
|
|
486
|
+
});
|
|
487
|
+
ctx.command("reject <groupId:string>", "拒绝群聊邀请", { authority: 4 }).action(async ({ session }, groupId) => {
|
|
488
|
+
if (!groupId) return "请指定群号。用法:reject <群号>";
|
|
489
|
+
if (!config.invite.adminQQs.includes(session.userId)) {
|
|
490
|
+
return "权限不足,只有管理员可以审核邀请。";
|
|
382
491
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
const channelId = guildId || session.channelId;
|
|
388
|
-
if (channelId) {
|
|
389
|
-
const originalMsg = await session.bot.getMessage(channelId, quoteId);
|
|
390
|
-
if (originalMsg?.content) {
|
|
391
|
-
quoteMessageContent = originalMsg.content;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (config.invite.showDetailedLog) {
|
|
396
|
-
console.error("通过消息ID获取引用消息内容失败:", error);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
492
|
+
const inviteData = pendingInvites.get(groupId);
|
|
493
|
+
if (!inviteData) {
|
|
494
|
+
return `未找到群号 ${groupId} 的待处理邀请。当前待处理邀请:${pendingInvites.size > 0 ? Array.from(pendingInvites.values()).map((i) => `${i.groupId}(${i.groupName})`).join(", ") : "无"}`;
|
|
400
495
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const extractedGroupId = groupIdMatch[1];
|
|
408
|
-
const extractedUserId = userIdMatch[1];
|
|
409
|
-
if (config.invite.showDetailedLog) {
|
|
410
|
-
console.log(`提取到群号: ${extractedGroupId}, QQ: ${extractedUserId}`);
|
|
411
|
-
}
|
|
412
|
-
let targetInviteId = null;
|
|
413
|
-
for (const [inviteId, inviteData] of pendingInvites) {
|
|
414
|
-
if (inviteData.groupId === extractedGroupId && inviteData.userId === extractedUserId) {
|
|
415
|
-
targetInviteId = inviteId;
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (targetInviteId) {
|
|
420
|
-
const inviteData = pendingInvites.get(targetInviteId);
|
|
421
|
-
if (inviteData) {
|
|
422
|
-
if (textContent === "同意" || textContent === "accept") {
|
|
423
|
-
try {
|
|
424
|
-
await session.bot.internal.setGroupAddRequest({
|
|
425
|
-
flag: inviteData.flag,
|
|
426
|
-
sub_type: "invite",
|
|
427
|
-
approve: true,
|
|
428
|
-
reason: ""
|
|
429
|
-
});
|
|
430
|
-
await session.send(`已同意加入群 ${inviteData.groupId}`);
|
|
431
|
-
try {
|
|
432
|
-
await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
console.error("通知邀请者失败:", error);
|
|
435
|
-
}
|
|
436
|
-
} catch (error) {
|
|
437
|
-
console.error("处理同意邀请失败:", error);
|
|
438
|
-
await session.send(`处理同意邀请失败: ${error.message}`);
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
try {
|
|
442
|
-
await session.bot.internal.setGroupAddRequest({
|
|
443
|
-
flag: inviteData.flag,
|
|
444
|
-
sub_type: "invite",
|
|
445
|
-
approve: false,
|
|
446
|
-
reason: "已拒绝"
|
|
447
|
-
});
|
|
448
|
-
await session.send(`已拒绝加入群 ${inviteData.groupId}`);
|
|
449
|
-
try {
|
|
450
|
-
await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请未通过管理员审核,机器人将不会加入该群聊。`);
|
|
451
|
-
} catch (error) {
|
|
452
|
-
console.error("通知邀请者失败:", error);
|
|
453
|
-
}
|
|
454
|
-
} catch (error) {
|
|
455
|
-
console.error("处理拒绝邀请失败:", error);
|
|
456
|
-
await session.send(`处理拒绝邀请失败: ${error.message}`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
pendingInvites.delete(targetInviteId);
|
|
460
|
-
}
|
|
461
|
-
} else if (config.invite.showDetailedLog) {
|
|
462
|
-
console.log(`未找到匹配的待处理邀请: 群号=${extractedGroupId}, QQ=${extractedUserId}`);
|
|
463
|
-
console.log(`当前待处理邀请列表:`, Array.from(pendingInvites.entries()));
|
|
496
|
+
try {
|
|
497
|
+
await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", false, "已拒绝");
|
|
498
|
+
try {
|
|
499
|
+
await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请未通过管理员审核,机器人将不会加入该群聊。`);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error("通知邀请者失败:", error);
|
|
464
502
|
}
|
|
465
|
-
|
|
466
|
-
|
|
503
|
+
pendingInvites.delete(groupId);
|
|
504
|
+
return `已拒绝加入群 ${groupId}(${inviteData.groupName}),邀请者:${inviteData.userName}`;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error("处理拒绝邀请失败:", error);
|
|
507
|
+
return `处理拒绝邀请失败: ${error.message}`;
|
|
467
508
|
}
|
|
468
509
|
});
|
|
510
|
+
ctx.command("pending-invites", "查看待处理的群聊邀请", { authority: 4 }).action(async ({ session }) => {
|
|
511
|
+
if (!config.invite.adminQQs.includes(session.userId)) {
|
|
512
|
+
return "权限不足,只有管理员可以查看待处理邀请。";
|
|
513
|
+
}
|
|
514
|
+
if (pendingInvites.size === 0) {
|
|
515
|
+
return "当前没有待处理的群聊邀请。";
|
|
516
|
+
}
|
|
517
|
+
const lines = ["待处理的群聊邀请列表:"];
|
|
518
|
+
for (const [, invite] of pendingInvites) {
|
|
519
|
+
const elapsed = Math.floor((Date.now() - invite.time) / 1e3 / 60);
|
|
520
|
+
lines.push(`- 群:${invite.groupName}(${invite.groupId})`);
|
|
521
|
+
lines.push(` 邀请者:${invite.userName}(${invite.userId})`);
|
|
522
|
+
lines.push(` ${elapsed} 分钟前`);
|
|
523
|
+
lines.push(` 同意:approve ${invite.groupId} | 拒绝:reject ${invite.groupId}`);
|
|
524
|
+
}
|
|
525
|
+
return lines.join("\n");
|
|
526
|
+
});
|
|
469
527
|
}
|
|
470
528
|
__name(apply3, "apply");
|
|
471
529
|
|
|
@@ -603,42 +661,62 @@ __export(switch_exports, {
|
|
|
603
661
|
});
|
|
604
662
|
var name6 = "group-control-switch";
|
|
605
663
|
function apply6(ctx, config) {
|
|
664
|
+
if (config.permission.protectedCommands?.length > 0) {
|
|
665
|
+
const protectedSet = new Set(config.permission.protectedCommands);
|
|
666
|
+
ctx.on("command/before-execute", async (argv) => {
|
|
667
|
+
const session = argv.session;
|
|
668
|
+
if (!session.guildId) return;
|
|
669
|
+
const commandName = argv.command.name;
|
|
670
|
+
if (!protectedSet.has(commandName)) return;
|
|
671
|
+
const hasPerm = await hasPermission(session, config);
|
|
672
|
+
if (!hasPerm) {
|
|
673
|
+
return "权限不足,只有群管理员可以使用此指令。";
|
|
674
|
+
}
|
|
675
|
+
}, true);
|
|
676
|
+
}
|
|
606
677
|
if (!config.botSwitch?.enabled) return;
|
|
607
|
-
|
|
678
|
+
const cmdOpts = {};
|
|
679
|
+
if (config.permission.mode === "koishi") {
|
|
680
|
+
cmdOpts.authority = config.permission.koishiAuthority;
|
|
681
|
+
}
|
|
682
|
+
ctx.command("bot-on", "开启机器人", cmdOpts).action(async ({ session }) => {
|
|
608
683
|
if (!session.guildId) return "该指令只能在群聊中使用。";
|
|
684
|
+
if (config.permission.mode === "builtin") {
|
|
685
|
+
const hasPerm = await hasPermission(session, config);
|
|
686
|
+
if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
|
|
687
|
+
}
|
|
609
688
|
await setGroupBotStatus(ctx, session.platform, session.guildId, true);
|
|
610
689
|
return "机器人已在此群开启。";
|
|
611
690
|
});
|
|
612
|
-
ctx.command("bot-off", "关闭机器人",
|
|
691
|
+
ctx.command("bot-off", "关闭机器人", cmdOpts).action(async ({ session }) => {
|
|
613
692
|
if (!session.guildId) return "该指令只能在群聊中使用。";
|
|
693
|
+
if (config.permission.mode === "builtin") {
|
|
694
|
+
const hasPerm = await hasPermission(session, config);
|
|
695
|
+
if (!hasPerm) return "权限不足,只有群管理员可以使用此指令。";
|
|
696
|
+
}
|
|
614
697
|
await setGroupBotStatus(ctx, session.platform, session.guildId, false);
|
|
615
|
-
return "
|
|
698
|
+
return "机器人已在此群关闭。所有指令和主动响应(入群欢迎、链接解析等)将被阻止。使用 bot-on 重新开启。";
|
|
616
699
|
});
|
|
617
|
-
ctx.on(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
} catch (e) {
|
|
633
|
-
ctx.logger("group-control-switch").warn("发送关闭提示失败", e);
|
|
634
|
-
}
|
|
700
|
+
ctx.on("command/before-execute", async (argv) => {
|
|
701
|
+
const session = argv.session;
|
|
702
|
+
if (!session.guildId) return;
|
|
703
|
+
if (ADMIN_COMMANDS.has(argv.command.name)) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const status = await getGroupBotStatus(ctx, session.platform, session.guildId);
|
|
707
|
+
const isBotEnabled = status ? status.botEnabled : config.botSwitch.defaultState;
|
|
708
|
+
if (!isBotEnabled) {
|
|
709
|
+
const isMentioned = session.elements?.some((e) => e.type === "at" && e.attrs.id === session.bot.userId);
|
|
710
|
+
if (isMentioned && config.botSwitch.disabledMessage) {
|
|
711
|
+
try {
|
|
712
|
+
await session.send(config.botSwitch.disabledMessage);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
ctx.logger("group-control-switch").warn("发送关闭提示失败", e);
|
|
635
715
|
}
|
|
636
|
-
return "";
|
|
637
716
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
);
|
|
717
|
+
return "";
|
|
718
|
+
}
|
|
719
|
+
}, true);
|
|
642
720
|
ctx.middleware(async (session, next) => {
|
|
643
721
|
if (!session.guildId) return next();
|
|
644
722
|
const status = await getGroupBotStatus(ctx, session.platform, session.guildId);
|
|
@@ -654,27 +732,37 @@ function apply6(ctx, config) {
|
|
|
654
732
|
}
|
|
655
733
|
}
|
|
656
734
|
return;
|
|
657
|
-
});
|
|
735
|
+
}, true);
|
|
658
736
|
}
|
|
659
737
|
__name(apply6, "apply");
|
|
660
738
|
|
|
661
739
|
// src/config.ts
|
|
662
740
|
var import_koishi = require("koishi");
|
|
663
741
|
var Config = import_koishi.Schema.intersect([
|
|
742
|
+
import_koishi.Schema.object({
|
|
743
|
+
permission: import_koishi.Schema.object({
|
|
744
|
+
mode: import_koishi.Schema.union([
|
|
745
|
+
import_koishi.Schema.const("koishi").description("使用 Koishi 自带权限系统 (authority)"),
|
|
746
|
+
import_koishi.Schema.const("builtin").description("使用插件内置权限管理 (群管理员/群主)")
|
|
747
|
+
]).default("builtin").description("权限管理模式"),
|
|
748
|
+
koishiAuthority: import_koishi.Schema.number().default(3).description("Koishi 模式下,管理指令所需的最低权限等级"),
|
|
749
|
+
protectedCommands: import_koishi.Schema.array(String).default([]).description("需要群管理员权限才能使用的自定义指令名列表(如来自其他插件的指令)")
|
|
750
|
+
}).description("权限管理")
|
|
751
|
+
}),
|
|
664
752
|
import_koishi.Schema.object({
|
|
665
753
|
basic: import_koishi.Schema.object({
|
|
666
754
|
welcomeMessage: import_koishi.Schema.string().default("你好,我是机器人。").description("机器人加入群聊时发送的欢迎消息"),
|
|
667
755
|
blacklistMessage: import_koishi.Schema.string().default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后在群内发送的提示"),
|
|
668
756
|
quitMessage: import_koishi.Schema.string().default("收到来自{userId}的指令,即将退出群聊。").description("用户发送quit指令后在群内发送的提示,支持变量{userId}"),
|
|
669
757
|
enableBlacklist: import_koishi.Schema.boolean().default(true).description('启用"被踢出自动拉黑"功能'),
|
|
758
|
+
quitCommandEnabled: import_koishi.Schema.boolean().default(true).description("启用quit"),
|
|
670
759
|
notifyAdminOnKick: import_koishi.Schema.boolean().default(true).description("被踢出群时通知管理员(需要在群聊邀请审核中配置管理员QQ)"),
|
|
671
760
|
kickNotificationMessage: import_koishi.Schema.string().default("机器人已被踢出群聊\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知消息模板,支持变量{groupId}"),
|
|
672
761
|
smallGroupAutoQuit: import_koishi.Schema.boolean().default(false).description("启用小群自动退群功能"),
|
|
673
762
|
smallGroupThreshold: import_koishi.Schema.number().default(30).description("小群人数阈值(群成员数小于等于此值时自动退群)"),
|
|
674
763
|
smallGroupQuitMessage: import_koishi.Schema.string().default("该群人数过少({memberCount}人),不满足最低人数要求({threshold}人),机器人将自动退出。").description("小群自动退群时在群内发送的提示,支持变量{memberCount}, {threshold}"),
|
|
675
764
|
smallGroupNotifyAdmin: import_koishi.Schema.boolean().default(true).description("小群自动退群时通知管理员"),
|
|
676
|
-
|
|
677
|
-
quitCommandAuthority: import_koishi.Schema.number().default(3).description("quit指令所需权限")
|
|
765
|
+
smallGroupCheckDelay: import_koishi.Schema.number().default(3e3).description("小群检测延迟(毫秒),加入群聊后等待一段时间再获取群信息以确保数据准确")
|
|
678
766
|
}).description("基础群组管理")
|
|
679
767
|
}),
|
|
680
768
|
import_koishi.Schema.object({
|
|
@@ -696,7 +784,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
696
784
|
adminQQs: import_koishi.Schema.array(String).default([]).description("管理员QQ号列表(用于权限验证)"),
|
|
697
785
|
notificationGroupId: import_koishi.Schema.string().description("通知群号(可选:若填写,邀请请求将发送到此群;若不填,则发送私聊给管理员)"),
|
|
698
786
|
inviteWaitMessage: import_koishi.Schema.string().default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发送给邀请者的等待审核提示消息"),
|
|
699
|
-
inviteRequestMessage: import_koishi.Schema.string().default(
|
|
787
|
+
inviteRequestMessage: import_koishi.Schema.string().default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请管理员使用指令 approve {groupId} 同意或 reject {groupId} 拒绝。").description("发送给管理员的邀请请求消息模板,支持变量{groupName}, {groupId}, {userName}, {userId}"),
|
|
700
788
|
autoApprove: import_koishi.Schema.boolean().default(false).description("是否自动同意邀请(仅在没有指定管理员时)"),
|
|
701
789
|
showDetailedLog: import_koishi.Schema.boolean().default(false).description("是否显示详细日志")
|
|
702
790
|
}).description("群聊邀请审核")
|
|
@@ -705,8 +793,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
705
793
|
botSwitch: import_koishi.Schema.object({
|
|
706
794
|
enabled: import_koishi.Schema.boolean().default(true).description("启用独立的群聊bot开关功能"),
|
|
707
795
|
defaultState: import_koishi.Schema.boolean().default(true).description("群聊中的默认开启状态"),
|
|
708
|
-
disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用bot-on开启。").description("机器人在关闭状态下被@时的提示消息")
|
|
709
|
-
toggleAuthority: import_koishi.Schema.number().default(3).description("开关Bot指令(bot-on/bot-off)所需权限")
|
|
796
|
+
disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用bot-on开启。").description("机器人在关闭状态下被@时的提示消息")
|
|
710
797
|
}).description("机器人开关控制")
|
|
711
798
|
})
|
|
712
799
|
]);
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { Session } from 'koishi';
|
|
1
2
|
import { Config } from './config';
|
|
2
3
|
export declare function isBlacklistEnabled(config: Config['basic']): string | null;
|
|
3
4
|
export declare function parseGuildId(input: string): string | null;
|
|
4
5
|
export declare function formatDate(timestamp: number): string;
|
|
5
6
|
export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* 检查用户是否有管理权限
|
|
9
|
+
* - koishi 模式: 使用 Koishi 自带的 authority 系统
|
|
10
|
+
* - builtin 模式: 检查用户是否为群管理员/群主,或在管理员QQ列表中
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasPermission(session: Session, config: Config): Promise<boolean>;
|
|
13
|
+
/** 管理指令列表 - 这些指令始终不受 bot-off 影响 */
|
|
14
|
+
export declare const ADMIN_COMMANDS: Set<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-group-control",
|
|
3
3
|
"description": "Koishi 插件,一个多功能的群聊自管理工具。支持被踢出自动拉黑、刷屏自动屏蔽、开关控制等功能。(仅支持 OneBot 适配器)",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"koishi": "^4.18.7"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|