koishi-plugin-group-control 1.0.3 → 1.0.5
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/config.d.ts +6 -0
- package/lib/database.d.ts +12 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +382 -141
- package/lib/utils.d.ts +1 -1
- package/package.json +1 -1
- package/readme.md +16 -2
- package/lib/state.d.ts +0 -6
package/lib/config.d.ts
CHANGED
|
@@ -12,10 +12,16 @@ export interface GroupConfig {
|
|
|
12
12
|
smallGroupQuitMessage: string;
|
|
13
13
|
smallGroupNotifyAdmin: boolean;
|
|
14
14
|
smallGroupCheckDelay: number;
|
|
15
|
+
smallGroupExcludeOfficialBots: boolean;
|
|
16
|
+
smallGroupRealtimeMonitor: boolean;
|
|
17
|
+
smallGroupRecheckCooldown: number;
|
|
15
18
|
smallGroupQualifiedNotifyAdmin: boolean;
|
|
16
19
|
smallGroupQualifiedMessage: string;
|
|
17
20
|
notifyAdminOnMute: boolean;
|
|
18
21
|
muteNotificationMessage: string;
|
|
22
|
+
muteAutoQuit: boolean;
|
|
23
|
+
muteAutoQuitThreshold: number;
|
|
24
|
+
muteQuitNotificationMessage: string;
|
|
19
25
|
}
|
|
20
26
|
export interface AdminConfig {
|
|
21
27
|
adminQQs: string[];
|
package/lib/database.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface SmallGroupWhitelist {
|
|
|
25
25
|
platform: string;
|
|
26
26
|
guildId: string;
|
|
27
27
|
}
|
|
28
|
+
export interface ApprovedGuild {
|
|
29
|
+
platform: string;
|
|
30
|
+
guildId: string;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
28
33
|
export interface SelfLeftGuild {
|
|
29
34
|
platform: string;
|
|
30
35
|
guildId: string;
|
|
@@ -54,6 +59,7 @@ declare module 'koishi' {
|
|
|
54
59
|
group_bot_status: GroupBotStatus;
|
|
55
60
|
small_group_whitelist: SmallGroupWhitelist;
|
|
56
61
|
self_left_guild: SelfLeftGuild;
|
|
62
|
+
approved_guild: ApprovedGuild;
|
|
57
63
|
pending_invite: PendingInvite;
|
|
58
64
|
pending_friend_request: PendingFriendRequest;
|
|
59
65
|
}
|
|
@@ -75,7 +81,7 @@ export declare function consumeSelfLeft(ctx: Context, guildId: string, maxAgeSec
|
|
|
75
81
|
/** 清理标记(退群失败时回滚,或 unban 时清理)*/
|
|
76
82
|
export declare function clearSelfLeft(ctx: Context, guildId: string): Promise<void>;
|
|
77
83
|
/** 定期清理过期的主动退群标记(超过 maxAgeSec 秒未消费的)*/
|
|
78
|
-
export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<
|
|
84
|
+
export declare function clearExpiredSelfLeft(ctx: Context, maxAgeSec?: number): Promise<void>;
|
|
79
85
|
export declare function getCommandFrequencyRecord(ctx: Context, platform: string, guildId: string): Promise<CommandFrequencyRecord>;
|
|
80
86
|
export declare function updateCommandFrequencyRecord(ctx: Context, platform: string, guildId: string, data: Partial<CommandFrequencyRecord>): Promise<void>;
|
|
81
87
|
export declare function getGroupBotStatus(ctx: Context, platform: string, guildId: string): Promise<GroupBotStatus | null>;
|
|
@@ -84,13 +90,16 @@ export declare function isInSmallGroupWhitelist(ctx: Context, guildId: string):
|
|
|
84
90
|
export declare function addToSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
|
|
85
91
|
export declare function removeFromSmallGroupWhitelist(ctx: Context, guildId: string): Promise<void>;
|
|
86
92
|
export declare function getAllSmallGroupWhitelist(ctx: Context): Promise<SmallGroupWhitelist[]>;
|
|
93
|
+
export declare function markApprovedGuild(ctx: Context, guildId: string): Promise<void>;
|
|
94
|
+
export declare function isApprovedGuild(ctx: Context, guildId: string): Promise<boolean>;
|
|
95
|
+
export declare function clearApprovedGuild(ctx: Context, guildId: string): Promise<void>;
|
|
87
96
|
export declare function getPendingInvite(ctx: Context, groupId: string): Promise<PendingInvite>;
|
|
88
97
|
export declare function addPendingInvite(ctx: Context, inviteUser: Omit<PendingInvite, 'platform'>): Promise<void>;
|
|
89
98
|
export declare function removePendingInvite(ctx: Context, groupId: string): Promise<void>;
|
|
90
99
|
export declare function getAllPendingInvites(ctx: Context): Promise<PendingInvite[]>;
|
|
91
|
-
export declare function clearExpiredPendingInvites(ctx: Context, expireTimeMs: number): Promise<
|
|
100
|
+
export declare function clearExpiredPendingInvites(ctx: Context, expireTimeMs: number): Promise<void>;
|
|
92
101
|
export declare function getPendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<PendingFriendRequest>;
|
|
93
102
|
export declare function addPendingFriendRequest(ctx: Context, platform: string, data: Omit<PendingFriendRequest, 'platform'>): Promise<void>;
|
|
94
103
|
export declare function removePendingFriendRequest(ctx: Context, platform: string, userId: string): Promise<void>;
|
|
95
104
|
export declare function getAllPendingFriendRequests(ctx: Context, platform: string): Promise<PendingFriendRequest[]>;
|
|
96
|
-
export declare function clearExpiredPendingFriendRequests(ctx: Context, platform: string, expireTimeMs: number): Promise<
|
|
105
|
+
export declare function clearExpiredPendingFriendRequests(ctx: Context, platform: string, expireTimeMs: number): Promise<void>;
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
Config: () => Config,
|
|
24
24
|
apply: () => apply8,
|
|
25
|
+
inject: () => inject,
|
|
25
26
|
name: () => name8
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -35,6 +36,7 @@ __export(database_exports, {
|
|
|
35
36
|
addToSmallGroupWhitelist: () => addToSmallGroupWhitelist,
|
|
36
37
|
apply: () => apply,
|
|
37
38
|
blacklistKicked: () => blacklistKicked,
|
|
39
|
+
clearApprovedGuild: () => clearApprovedGuild,
|
|
38
40
|
clearBlacklistedGuilds: () => clearBlacklistedGuilds,
|
|
39
41
|
clearExpiredPendingFriendRequests: () => clearExpiredPendingFriendRequests,
|
|
40
42
|
clearExpiredPendingInvites: () => clearExpiredPendingInvites,
|
|
@@ -51,7 +53,9 @@ __export(database_exports, {
|
|
|
51
53
|
getGroupBotStatus: () => getGroupBotStatus,
|
|
52
54
|
getPendingFriendRequest: () => getPendingFriendRequest,
|
|
53
55
|
getPendingInvite: () => getPendingInvite,
|
|
56
|
+
isApprovedGuild: () => isApprovedGuild,
|
|
54
57
|
isInSmallGroupWhitelist: () => isInSmallGroupWhitelist,
|
|
58
|
+
markApprovedGuild: () => markApprovedGuild,
|
|
55
59
|
markSelfLeft: () => markSelfLeft,
|
|
56
60
|
name: () => name,
|
|
57
61
|
removeBlacklistedGuild: () => removeBlacklistedGuild,
|
|
@@ -94,6 +98,11 @@ function apply(ctx) {
|
|
|
94
98
|
guildId: "string",
|
|
95
99
|
timestamp: "integer"
|
|
96
100
|
}, { primary: ["platform", "guildId"] });
|
|
101
|
+
ctx.model.extend("approved_guild", {
|
|
102
|
+
platform: "string",
|
|
103
|
+
guildId: "string",
|
|
104
|
+
timestamp: "integer"
|
|
105
|
+
}, { primary: ["platform", "guildId"] });
|
|
97
106
|
ctx.model.extend("pending_invite", {
|
|
98
107
|
platform: "string",
|
|
99
108
|
groupId: "string",
|
|
@@ -115,15 +124,15 @@ function apply(ctx) {
|
|
|
115
124
|
__name(apply, "apply");
|
|
116
125
|
var BLACKLIST_PLATFORM = "onebot";
|
|
117
126
|
async function getBlacklistedGuild(ctx, guildId) {
|
|
118
|
-
return await ctx.
|
|
127
|
+
return await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
119
128
|
}
|
|
120
129
|
__name(getBlacklistedGuild, "getBlacklistedGuild");
|
|
121
130
|
async function removeBlacklistedGuild(ctx, guildId) {
|
|
122
|
-
return await ctx.
|
|
131
|
+
return await ctx.database.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
123
132
|
}
|
|
124
133
|
__name(removeBlacklistedGuild, "removeBlacklistedGuild");
|
|
125
134
|
async function createBlacklistedGuild(ctx, guildId, reason) {
|
|
126
|
-
return await ctx.
|
|
135
|
+
return await ctx.database.upsert("blacklisted_guild", [{
|
|
127
136
|
platform: BLACKLIST_PLATFORM,
|
|
128
137
|
guildId,
|
|
129
138
|
timestamp: Math.floor(Date.now() / 1e3),
|
|
@@ -132,15 +141,15 @@ async function createBlacklistedGuild(ctx, guildId, reason) {
|
|
|
132
141
|
}
|
|
133
142
|
__name(createBlacklistedGuild, "createBlacklistedGuild");
|
|
134
143
|
async function getAllBlacklistedGuilds(ctx) {
|
|
135
|
-
return await ctx.
|
|
144
|
+
return await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
|
|
136
145
|
}
|
|
137
146
|
__name(getAllBlacklistedGuilds, "getAllBlacklistedGuilds");
|
|
138
147
|
async function clearBlacklistedGuilds(ctx) {
|
|
139
|
-
return await ctx.
|
|
148
|
+
return await ctx.database.remove("blacklisted_guild", { platform: BLACKLIST_PLATFORM });
|
|
140
149
|
}
|
|
141
150
|
__name(clearBlacklistedGuilds, "clearBlacklistedGuilds");
|
|
142
151
|
async function blacklistKicked(ctx, guildId) {
|
|
143
|
-
return await ctx.
|
|
152
|
+
return await ctx.database.upsert("blacklisted_guild", [{
|
|
144
153
|
platform: BLACKLIST_PLATFORM,
|
|
145
154
|
guildId,
|
|
146
155
|
timestamp: Math.floor(Date.now() / 1e3),
|
|
@@ -149,7 +158,7 @@ async function blacklistKicked(ctx, guildId) {
|
|
|
149
158
|
}
|
|
150
159
|
__name(blacklistKicked, "blacklistKicked");
|
|
151
160
|
async function markSelfLeft(ctx, guildId) {
|
|
152
|
-
await ctx.
|
|
161
|
+
await ctx.database.upsert("self_left_guild", [{
|
|
153
162
|
platform: BLACKLIST_PLATFORM,
|
|
154
163
|
guildId,
|
|
155
164
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
@@ -157,33 +166,28 @@ async function markSelfLeft(ctx, guildId) {
|
|
|
157
166
|
}
|
|
158
167
|
__name(markSelfLeft, "markSelfLeft");
|
|
159
168
|
async function consumeSelfLeft(ctx, guildId, maxAgeSec = 120) {
|
|
160
|
-
const [row] = await ctx.
|
|
169
|
+
const [row] = await ctx.database.get("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
161
170
|
if (!row) return false;
|
|
162
|
-
await ctx.
|
|
171
|
+
await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
163
172
|
return Math.floor(Date.now() / 1e3) - row.timestamp <= maxAgeSec;
|
|
164
173
|
}
|
|
165
174
|
__name(consumeSelfLeft, "consumeSelfLeft");
|
|
166
175
|
async function clearSelfLeft(ctx, guildId) {
|
|
167
|
-
await ctx.
|
|
176
|
+
await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
168
177
|
}
|
|
169
178
|
__name(clearSelfLeft, "clearSelfLeft");
|
|
170
179
|
async function clearExpiredSelfLeft(ctx, maxAgeSec = 300) {
|
|
171
180
|
const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSec;
|
|
172
|
-
|
|
173
|
-
const expired = all.filter((r) => r.timestamp < cutoff);
|
|
174
|
-
for (const record of expired) {
|
|
175
|
-
await ctx.model.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, guildId: record.guildId });
|
|
176
|
-
}
|
|
177
|
-
return expired.length;
|
|
181
|
+
await ctx.database.remove("self_left_guild", { platform: BLACKLIST_PLATFORM, timestamp: { $lt: cutoff } });
|
|
178
182
|
}
|
|
179
183
|
__name(clearExpiredSelfLeft, "clearExpiredSelfLeft");
|
|
180
184
|
async function getCommandFrequencyRecord(ctx, platform, guildId) {
|
|
181
|
-
const records = await ctx.
|
|
185
|
+
const records = await ctx.database.get("command_frequency_record", { platform, guildId });
|
|
182
186
|
return records.length > 0 ? records[0] : null;
|
|
183
187
|
}
|
|
184
188
|
__name(getCommandFrequencyRecord, "getCommandFrequencyRecord");
|
|
185
189
|
async function updateCommandFrequencyRecord(ctx, platform, guildId, data) {
|
|
186
|
-
await ctx.
|
|
190
|
+
await ctx.database.upsert("command_frequency_record", [{
|
|
187
191
|
platform,
|
|
188
192
|
guildId,
|
|
189
193
|
...data
|
|
@@ -195,86 +199,93 @@ var groupBotStatusCacheKey = /* @__PURE__ */ __name((platform, guildId) => `${pl
|
|
|
195
199
|
async function getGroupBotStatus(ctx, platform, guildId) {
|
|
196
200
|
const key = groupBotStatusCacheKey(platform, guildId);
|
|
197
201
|
if (groupBotStatusCache.has(key)) return groupBotStatusCache.get(key);
|
|
198
|
-
const records = await ctx.
|
|
202
|
+
const records = await ctx.database.get("group_bot_status", { platform, guildId });
|
|
199
203
|
const status = records.length > 0 ? records[0] : null;
|
|
200
204
|
groupBotStatusCache.set(key, status);
|
|
201
205
|
return status;
|
|
202
206
|
}
|
|
203
207
|
__name(getGroupBotStatus, "getGroupBotStatus");
|
|
204
208
|
async function setGroupBotStatus(ctx, platform, guildId, botEnabled) {
|
|
205
|
-
await ctx.
|
|
209
|
+
await ctx.database.upsert("group_bot_status", [{ platform, guildId, botEnabled }]);
|
|
206
210
|
groupBotStatusCache.set(groupBotStatusCacheKey(platform, guildId), { platform, guildId, botEnabled });
|
|
207
211
|
}
|
|
208
212
|
__name(setGroupBotStatus, "setGroupBotStatus");
|
|
209
213
|
async function isInSmallGroupWhitelist(ctx, guildId) {
|
|
210
|
-
const records = await ctx.
|
|
214
|
+
const records = await ctx.database.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
|
|
211
215
|
return records.length > 0;
|
|
212
216
|
}
|
|
213
217
|
__name(isInSmallGroupWhitelist, "isInSmallGroupWhitelist");
|
|
214
218
|
async function addToSmallGroupWhitelist(ctx, guildId) {
|
|
215
|
-
await ctx.
|
|
219
|
+
await ctx.database.upsert("small_group_whitelist", [{ platform: BLACKLIST_PLATFORM, guildId }]);
|
|
216
220
|
}
|
|
217
221
|
__name(addToSmallGroupWhitelist, "addToSmallGroupWhitelist");
|
|
218
222
|
async function removeFromSmallGroupWhitelist(ctx, guildId) {
|
|
219
|
-
await ctx.
|
|
223
|
+
await ctx.database.remove("small_group_whitelist", { platform: BLACKLIST_PLATFORM, guildId });
|
|
220
224
|
}
|
|
221
225
|
__name(removeFromSmallGroupWhitelist, "removeFromSmallGroupWhitelist");
|
|
222
226
|
async function getAllSmallGroupWhitelist(ctx) {
|
|
223
|
-
return await ctx.
|
|
227
|
+
return await ctx.database.get("small_group_whitelist", { platform: BLACKLIST_PLATFORM });
|
|
224
228
|
}
|
|
225
229
|
__name(getAllSmallGroupWhitelist, "getAllSmallGroupWhitelist");
|
|
230
|
+
async function markApprovedGuild(ctx, guildId) {
|
|
231
|
+
await ctx.database.upsert("approved_guild", [{
|
|
232
|
+
platform: BLACKLIST_PLATFORM,
|
|
233
|
+
guildId,
|
|
234
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
235
|
+
}]);
|
|
236
|
+
}
|
|
237
|
+
__name(markApprovedGuild, "markApprovedGuild");
|
|
238
|
+
async function isApprovedGuild(ctx, guildId) {
|
|
239
|
+
const records = await ctx.database.get("approved_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
240
|
+
return records.length > 0;
|
|
241
|
+
}
|
|
242
|
+
__name(isApprovedGuild, "isApprovedGuild");
|
|
243
|
+
async function clearApprovedGuild(ctx, guildId) {
|
|
244
|
+
await ctx.database.remove("approved_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
245
|
+
}
|
|
246
|
+
__name(clearApprovedGuild, "clearApprovedGuild");
|
|
226
247
|
async function getPendingInvite(ctx, groupId) {
|
|
227
|
-
const records = await ctx.
|
|
248
|
+
const records = await ctx.database.get("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
|
|
228
249
|
return records.length > 0 ? records[0] : null;
|
|
229
250
|
}
|
|
230
251
|
__name(getPendingInvite, "getPendingInvite");
|
|
231
252
|
async function addPendingInvite(ctx, inviteUser) {
|
|
232
|
-
await ctx.
|
|
253
|
+
await ctx.database.upsert("pending_invite", [{ platform: BLACKLIST_PLATFORM, ...inviteUser }]);
|
|
233
254
|
}
|
|
234
255
|
__name(addPendingInvite, "addPendingInvite");
|
|
235
256
|
async function removePendingInvite(ctx, groupId) {
|
|
236
|
-
await ctx.
|
|
257
|
+
await ctx.database.remove("pending_invite", { platform: BLACKLIST_PLATFORM, groupId });
|
|
237
258
|
}
|
|
238
259
|
__name(removePendingInvite, "removePendingInvite");
|
|
239
260
|
async function getAllPendingInvites(ctx) {
|
|
240
|
-
return await ctx.
|
|
261
|
+
return await ctx.database.get("pending_invite", { platform: BLACKLIST_PLATFORM });
|
|
241
262
|
}
|
|
242
263
|
__name(getAllPendingInvites, "getAllPendingInvites");
|
|
243
264
|
async function clearExpiredPendingInvites(ctx, expireTimeMs) {
|
|
244
265
|
const cutoff = Math.floor((Date.now() - expireTimeMs) / 1e3);
|
|
245
|
-
|
|
246
|
-
const expired = all.filter((r) => r.time < cutoff);
|
|
247
|
-
for (const record of expired) {
|
|
248
|
-
await ctx.model.remove("pending_invite", { platform: BLACKLIST_PLATFORM, groupId: record.groupId });
|
|
249
|
-
}
|
|
250
|
-
return expired.length;
|
|
266
|
+
await ctx.database.remove("pending_invite", { platform: BLACKLIST_PLATFORM, time: { $lt: cutoff } });
|
|
251
267
|
}
|
|
252
268
|
__name(clearExpiredPendingInvites, "clearExpiredPendingInvites");
|
|
253
269
|
async function getPendingFriendRequest(ctx, platform, userId) {
|
|
254
|
-
const records = await ctx.
|
|
270
|
+
const records = await ctx.database.get("pending_friend_request", { platform, userId });
|
|
255
271
|
return records.length > 0 ? records[0] : null;
|
|
256
272
|
}
|
|
257
273
|
__name(getPendingFriendRequest, "getPendingFriendRequest");
|
|
258
274
|
async function addPendingFriendRequest(ctx, platform, data) {
|
|
259
|
-
await ctx.
|
|
275
|
+
await ctx.database.upsert("pending_friend_request", [{ platform, ...data }]);
|
|
260
276
|
}
|
|
261
277
|
__name(addPendingFriendRequest, "addPendingFriendRequest");
|
|
262
278
|
async function removePendingFriendRequest(ctx, platform, userId) {
|
|
263
|
-
await ctx.
|
|
279
|
+
await ctx.database.remove("pending_friend_request", { platform, userId });
|
|
264
280
|
}
|
|
265
281
|
__name(removePendingFriendRequest, "removePendingFriendRequest");
|
|
266
282
|
async function getAllPendingFriendRequests(ctx, platform) {
|
|
267
|
-
return await ctx.
|
|
283
|
+
return await ctx.database.get("pending_friend_request", { platform });
|
|
268
284
|
}
|
|
269
285
|
__name(getAllPendingFriendRequests, "getAllPendingFriendRequests");
|
|
270
286
|
async function clearExpiredPendingFriendRequests(ctx, platform, expireTimeMs) {
|
|
271
287
|
const cutoff = Math.floor((Date.now() - expireTimeMs) / 1e3);
|
|
272
|
-
|
|
273
|
-
const expired = all.filter((r) => r.time < cutoff);
|
|
274
|
-
for (const record of expired) {
|
|
275
|
-
await ctx.model.remove("pending_friend_request", { platform, userId: record.userId });
|
|
276
|
-
}
|
|
277
|
-
return expired.length;
|
|
288
|
+
await ctx.database.remove("pending_friend_request", { platform, time: { $lt: cutoff } });
|
|
278
289
|
}
|
|
279
290
|
__name(clearExpiredPendingFriendRequests, "clearExpiredPendingFriendRequests");
|
|
280
291
|
|
|
@@ -292,6 +303,7 @@ function isBlacklistEnabled(config) {
|
|
|
292
303
|
}
|
|
293
304
|
__name(isBlacklistEnabled, "isBlacklistEnabled");
|
|
294
305
|
function parseGuildId(input) {
|
|
306
|
+
if (!input) return null;
|
|
295
307
|
const match = input.trim().match(/^onebot:(\d+)$/);
|
|
296
308
|
return match ? match[1] : /^\d+$/.test(input.trim()) ? input.trim() : null;
|
|
297
309
|
}
|
|
@@ -376,20 +388,22 @@ var ADMIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
376
388
|
"gc.sg-add",
|
|
377
389
|
"gc.sg-rm",
|
|
378
390
|
"gc.sg-list",
|
|
391
|
+
"gc.friends",
|
|
392
|
+
"gc.delfriend",
|
|
393
|
+
"gc.groups",
|
|
394
|
+
"gc.leave",
|
|
379
395
|
"gc.fp",
|
|
380
396
|
"gc.fa",
|
|
381
397
|
"gc.fr"
|
|
382
398
|
]);
|
|
383
399
|
|
|
384
|
-
// src/state.ts
|
|
385
|
-
var approvedGroups = /* @__PURE__ */ new Set();
|
|
386
|
-
|
|
387
400
|
// src/modules/basic.ts
|
|
388
401
|
var name2 = "group-control-basic";
|
|
389
402
|
async function getGroupName(bot, guildId) {
|
|
390
403
|
try {
|
|
391
404
|
const info = await bot.internal?.getGroupInfo?.(parseInt(guildId));
|
|
392
|
-
|
|
405
|
+
const data = info?.data ?? info;
|
|
406
|
+
if (data?.group_name) return data.group_name;
|
|
393
407
|
} catch {
|
|
394
408
|
}
|
|
395
409
|
try {
|
|
@@ -404,8 +418,12 @@ __name(getGroupName, "getGroupName");
|
|
|
404
418
|
function apply2(ctx, config) {
|
|
405
419
|
const quittingGuilds = /* @__PURE__ */ new Map();
|
|
406
420
|
const processedKicks = /* @__PURE__ */ new Map();
|
|
421
|
+
const processedAdds = /* @__PURE__ */ new Map();
|
|
422
|
+
const realtimeLastCheck = /* @__PURE__ */ new Map();
|
|
407
423
|
const QUITTING_EXPIRE_MS = 60 * 1e3;
|
|
408
424
|
const KICK_DEDUP_MS = 60 * 1e3;
|
|
425
|
+
const ADD_DEDUP_MS = 10 * 1e3;
|
|
426
|
+
const REALTIME_DEBOUNCE_MS = 1500;
|
|
409
427
|
setInterval(() => {
|
|
410
428
|
const now = Date.now();
|
|
411
429
|
for (const [key, time] of quittingGuilds) {
|
|
@@ -414,6 +432,12 @@ function apply2(ctx, config) {
|
|
|
414
432
|
for (const [key, time] of processedKicks) {
|
|
415
433
|
if (now - time > KICK_DEDUP_MS) processedKicks.delete(key);
|
|
416
434
|
}
|
|
435
|
+
for (const [key, time] of processedAdds) {
|
|
436
|
+
if (now - time > ADD_DEDUP_MS) processedAdds.delete(key);
|
|
437
|
+
}
|
|
438
|
+
for (const [key, time] of realtimeLastCheck) {
|
|
439
|
+
if (now - time > 5 * 60 * 1e3) realtimeLastCheck.delete(key);
|
|
440
|
+
}
|
|
417
441
|
}, 30 * 1e3);
|
|
418
442
|
setInterval(async () => {
|
|
419
443
|
try {
|
|
@@ -421,11 +445,101 @@ function apply2(ctx, config) {
|
|
|
421
445
|
} catch {
|
|
422
446
|
}
|
|
423
447
|
}, 5 * 60 * 1e3);
|
|
448
|
+
async function evaluateSmallGroup(bot, guildId) {
|
|
449
|
+
const threshold = config.basic.smallGroupThreshold;
|
|
450
|
+
const exclude = config.basic.smallGroupExcludeOfficialBots;
|
|
451
|
+
const maxBots = 20;
|
|
452
|
+
let total = 0;
|
|
453
|
+
let groupName = "未知";
|
|
454
|
+
try {
|
|
455
|
+
const info = await bot.internal?.getGroupInfo?.(parseInt(guildId));
|
|
456
|
+
const data = info?.data ?? info;
|
|
457
|
+
total = Number(data?.member_count) || 0;
|
|
458
|
+
if (data?.group_name) groupName = data.group_name;
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
if (total === 0) {
|
|
462
|
+
try {
|
|
463
|
+
const guildInfo = await bot.getGuild(guildId);
|
|
464
|
+
total = Number(guildInfo?.member_count ?? guildInfo?.memberCount) || 0;
|
|
465
|
+
if (guildInfo?.name) groupName = guildInfo.name;
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (total > 0 && total <= threshold) {
|
|
470
|
+
return { decision: "quit", count: total, groupName };
|
|
471
|
+
}
|
|
472
|
+
if (!exclude) {
|
|
473
|
+
if (total > 0) return { decision: total <= threshold ? "quit" : "keep", count: total, groupName };
|
|
474
|
+
} else if (total > threshold + maxBots) {
|
|
475
|
+
return { decision: "keep", count: total, groupName };
|
|
476
|
+
}
|
|
477
|
+
let list = [];
|
|
478
|
+
try {
|
|
479
|
+
const raw = await bot.internal?.getGroupMemberList?.(parseInt(guildId));
|
|
480
|
+
list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
if (list.length === 0) {
|
|
484
|
+
if (total > 0) return { decision: total <= threshold ? "quit" : "keep", count: total, groupName };
|
|
485
|
+
return { decision: "unknown", count: 0, groupName };
|
|
486
|
+
}
|
|
487
|
+
const N = list.length;
|
|
488
|
+
if (!exclude) return { decision: N <= threshold ? "quit" : "keep", count: N, groupName };
|
|
489
|
+
if (N <= threshold) return { decision: "quit", count: N, groupName };
|
|
490
|
+
const selfId = String(bot.selfId ?? "");
|
|
491
|
+
const botsNeeded = N - threshold;
|
|
492
|
+
let bots = 0;
|
|
493
|
+
for (const m of list) {
|
|
494
|
+
const uid = String(m?.user_id ?? m?.userId ?? "");
|
|
495
|
+
if (m?.is_robot === true || selfId && uid === selfId) {
|
|
496
|
+
bots++;
|
|
497
|
+
if (bots >= botsNeeded) {
|
|
498
|
+
return { decision: "quit", count: N - bots, groupName };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const real = N - bots;
|
|
503
|
+
return { decision: real <= threshold ? "quit" : "keep", count: real, groupName };
|
|
504
|
+
}
|
|
505
|
+
__name(evaluateSmallGroup, "evaluateSmallGroup");
|
|
506
|
+
async function performSmallGroupQuit(bot, platform, guildId, groupName, memberCount) {
|
|
507
|
+
const threshold = config.basic.smallGroupThreshold;
|
|
508
|
+
const quitMsg = config.basic.smallGroupQuitMessage.replaceAll("{memberCount}", memberCount.toString()).replaceAll("{threshold}", threshold.toString()).replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId);
|
|
509
|
+
try {
|
|
510
|
+
await bot.sendMessage(guildId, quitMsg, platform);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
}
|
|
513
|
+
if (config.basic.smallGroupNotifyAdmin) {
|
|
514
|
+
const adminMsg = `小群自动退群
|
|
515
|
+
群名称:${groupName}
|
|
516
|
+
群号:${guildId}
|
|
517
|
+
群成员数:${memberCount}人(阈值:${threshold}人)
|
|
518
|
+
机器人已自动退出该群。`;
|
|
519
|
+
await notifyAdmins(bot, config, adminMsg);
|
|
520
|
+
}
|
|
521
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
522
|
+
await markSelfLeft(ctx, guildId);
|
|
523
|
+
try {
|
|
524
|
+
await bot.internal.setGroupLeave(parseInt(guildId));
|
|
525
|
+
} catch (e) {
|
|
526
|
+
console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
|
|
527
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
528
|
+
await clearSelfLeft(ctx, guildId);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
__name(performSmallGroupQuit, "performSmallGroupQuit");
|
|
424
532
|
ctx.on("guild-added", async (session) => {
|
|
425
533
|
const { guildId, platform } = session;
|
|
426
534
|
ctx.logger("group-control-basic").info(`[guild-added] 触发!guildId=${guildId}, platform=${platform}`);
|
|
535
|
+
const addKey = `${BLACKLIST_PLATFORM}:${guildId}`;
|
|
536
|
+
if (processedAdds.has(addKey)) {
|
|
537
|
+
ctx.logger("group-control-basic").info(`[guild-added] 忽略重复事件 guildId=${guildId}`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
processedAdds.set(addKey, Date.now());
|
|
427
541
|
if (config.basic.enableBlacklist) {
|
|
428
|
-
const [blacklisted] = await ctx.
|
|
542
|
+
const [blacklisted] = await ctx.database.get("blacklisted_guild", { platform: BLACKLIST_PLATFORM, guildId });
|
|
429
543
|
if (blacklisted) {
|
|
430
544
|
try {
|
|
431
545
|
await session.bot.sendMessage(guildId, config.basic.blacklistMessage, platform);
|
|
@@ -442,8 +556,7 @@ function apply2(ctx, config) {
|
|
|
442
556
|
}
|
|
443
557
|
if (config.basic.smallGroupAutoQuit) {
|
|
444
558
|
const inWhitelist = await isInSmallGroupWhitelist(ctx, guildId);
|
|
445
|
-
const wasApproved =
|
|
446
|
-
if (wasApproved) approvedGroups.delete(guildId);
|
|
559
|
+
const wasApproved = await isApprovedGuild(ctx, guildId);
|
|
447
560
|
const pendingInvite = await getPendingInvite(ctx, guildId);
|
|
448
561
|
const hadPendingInvite = !!pendingInvite;
|
|
449
562
|
if (hadPendingInvite) await removePendingInvite(ctx, guildId);
|
|
@@ -452,58 +565,14 @@ function apply2(ctx, config) {
|
|
|
452
565
|
const delay = config.basic.smallGroupCheckDelay || 3e3;
|
|
453
566
|
setTimeout(async () => {
|
|
454
567
|
try {
|
|
455
|
-
|
|
456
|
-
let groupName =
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
} catch {
|
|
462
|
-
}
|
|
463
|
-
if (memberCount === 0) {
|
|
464
|
-
try {
|
|
465
|
-
const guildInfo = await session.bot.getGuild(guildId);
|
|
466
|
-
memberCount = guildInfo?.member_count || guildInfo?.memberCount || 0;
|
|
467
|
-
if (guildInfo?.name) groupName = guildInfo.name;
|
|
468
|
-
} catch {
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (memberCount === 0) {
|
|
472
|
-
try {
|
|
473
|
-
const memberList = await session.bot.getGuildMemberList(guildId);
|
|
474
|
-
memberCount = memberList?.data?.length || 0;
|
|
475
|
-
} catch {
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (groupName === "未知") {
|
|
479
|
-
groupName = await getGroupName(session.bot, guildId);
|
|
480
|
-
}
|
|
481
|
-
if (memberCount > 0 && memberCount <= config.basic.smallGroupThreshold) {
|
|
482
|
-
const quitMsg = config.basic.smallGroupQuitMessage.replaceAll("{memberCount}", memberCount.toString()).replaceAll("{threshold}", config.basic.smallGroupThreshold.toString()).replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId);
|
|
483
|
-
try {
|
|
484
|
-
await session.bot.sendMessage(guildId, quitMsg, platform);
|
|
485
|
-
} catch (e) {
|
|
486
|
-
}
|
|
487
|
-
if (config.basic.smallGroupNotifyAdmin) {
|
|
488
|
-
const adminMsg = `小群自动退群
|
|
489
|
-
群名称:${groupName}
|
|
490
|
-
群号:${guildId}
|
|
491
|
-
群成员数:${memberCount}人(阈值:${config.basic.smallGroupThreshold}人)
|
|
492
|
-
机器人已自动退出该群。`;
|
|
493
|
-
await notifyAdmins(session.bot, config, adminMsg);
|
|
494
|
-
}
|
|
495
|
-
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
496
|
-
await markSelfLeft(ctx, guildId);
|
|
497
|
-
try {
|
|
498
|
-
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
499
|
-
} catch (e) {
|
|
500
|
-
console.error(`小群自动退群失败 (群号: ${guildId}):`, e);
|
|
501
|
-
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
502
|
-
await clearSelfLeft(ctx, guildId);
|
|
503
|
-
}
|
|
504
|
-
} else if (memberCount > config.basic.smallGroupThreshold) {
|
|
568
|
+
const res = await evaluateSmallGroup(session.bot, guildId);
|
|
569
|
+
let groupName = res.groupName;
|
|
570
|
+
if (groupName === "未知") groupName = await getGroupName(session.bot, guildId);
|
|
571
|
+
if (res.decision === "quit") {
|
|
572
|
+
await performSmallGroupQuit(session.bot, platform, guildId, groupName, res.count);
|
|
573
|
+
} else if (res.decision === "keep") {
|
|
505
574
|
if (config.basic.smallGroupQualifiedNotifyAdmin) {
|
|
506
|
-
const qualifiedMsg = config.basic.smallGroupQualifiedMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId).replaceAll("{memberCount}",
|
|
575
|
+
const qualifiedMsg = config.basic.smallGroupQualifiedMessage.replaceAll("{groupName}", groupName).replaceAll("{groupId}", guildId).replaceAll("{memberCount}", res.count.toString()).replaceAll("{threshold}", config.basic.smallGroupThreshold.toString());
|
|
507
576
|
await notifyAdmins(session.bot, config, qualifiedMsg);
|
|
508
577
|
}
|
|
509
578
|
}
|
|
@@ -520,10 +589,41 @@ function apply2(ctx, config) {
|
|
|
520
589
|
}
|
|
521
590
|
}
|
|
522
591
|
});
|
|
592
|
+
if (config.basic.smallGroupAutoQuit && config.basic.smallGroupRealtimeMonitor) {
|
|
593
|
+
ctx.on("guild-member-removed", async (session) => {
|
|
594
|
+
const { guildId, platform } = session;
|
|
595
|
+
if (!guildId) return;
|
|
596
|
+
if (String(session.userId) === String(session.bot.selfId)) return;
|
|
597
|
+
if (quittingGuilds.has(`${BLACKLIST_PLATFORM}:${guildId}`)) return;
|
|
598
|
+
const cooldown = (config.basic.smallGroupRecheckCooldown || 60) * 1e3;
|
|
599
|
+
const now = Date.now();
|
|
600
|
+
if (now - (realtimeLastCheck.get(guildId) || 0) < cooldown) return;
|
|
601
|
+
realtimeLastCheck.set(guildId, now);
|
|
602
|
+
if (await isInSmallGroupWhitelist(ctx, guildId)) return;
|
|
603
|
+
if (await isApprovedGuild(ctx, guildId)) return;
|
|
604
|
+
setTimeout(async () => {
|
|
605
|
+
try {
|
|
606
|
+
if (quittingGuilds.has(`${BLACKLIST_PLATFORM}:${guildId}`)) return;
|
|
607
|
+
const res = await evaluateSmallGroup(session.bot, guildId);
|
|
608
|
+
if (res.decision === "quit") {
|
|
609
|
+
let groupName = res.groupName;
|
|
610
|
+
if (groupName === "未知") groupName = await getGroupName(session.bot, guildId);
|
|
611
|
+
await performSmallGroupQuit(session.bot, platform, guildId, groupName, res.count);
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
console.error(`实时小群检测失败 (群号: ${guildId}):`, error);
|
|
615
|
+
}
|
|
616
|
+
}, REALTIME_DEBOUNCE_MS);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
523
619
|
ctx.on("guild-removed", async (session) => {
|
|
524
620
|
const { guildId } = session;
|
|
525
621
|
const platform = BLACKLIST_PLATFORM;
|
|
526
622
|
const dedupKey = `${platform}:${guildId}`;
|
|
623
|
+
try {
|
|
624
|
+
await clearApprovedGuild(ctx, guildId);
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
527
627
|
if (quittingGuilds.has(dedupKey)) {
|
|
528
628
|
try {
|
|
529
629
|
await clearSelfLeft(ctx, guildId);
|
|
@@ -542,7 +642,7 @@ function apply2(ctx, config) {
|
|
|
542
642
|
if (processedKicks.has(dedupKey)) return;
|
|
543
643
|
processedKicks.set(dedupKey, Date.now());
|
|
544
644
|
if (config.basic.enableBlacklist || config.basic.notifyAdminOnKick) {
|
|
545
|
-
const [existing] = await ctx.
|
|
645
|
+
const [existing] = await ctx.database.get("blacklisted_guild", { platform, guildId });
|
|
546
646
|
if (existing && existing.reason === "kicked") {
|
|
547
647
|
return;
|
|
548
648
|
}
|
|
@@ -556,7 +656,7 @@ function apply2(ctx, config) {
|
|
|
556
656
|
await notifyAdmins(session.bot, config, kickMsg);
|
|
557
657
|
}
|
|
558
658
|
});
|
|
559
|
-
if (config.basic.notifyAdminOnMute) {
|
|
659
|
+
if (config.basic.notifyAdminOnMute || config.basic.muteAutoQuit) {
|
|
560
660
|
ctx.on("guild-member-mute", async (session) => {
|
|
561
661
|
if (session.userId !== session.bot?.userId) return;
|
|
562
662
|
if (!session.duration) return;
|
|
@@ -564,8 +664,28 @@ function apply2(ctx, config) {
|
|
|
564
664
|
const operatorId = session.operatorId || "未知";
|
|
565
665
|
const duration = session.duration ?? 0;
|
|
566
666
|
const groupName = await getGroupName(session.bot, guildId);
|
|
567
|
-
|
|
568
|
-
|
|
667
|
+
if (config.basic.muteAutoQuit && duration >= config.basic.muteAutoQuitThreshold) {
|
|
668
|
+
const quitMsg = config.basic.muteQuitNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName).replaceAll("{operatorId}", operatorId).replaceAll("{duration}", duration.toString());
|
|
669
|
+
await notifyAdmins(session.bot, config, quitMsg);
|
|
670
|
+
try {
|
|
671
|
+
await createBlacklistedGuild(ctx, guildId, "muted");
|
|
672
|
+
} catch (e) {
|
|
673
|
+
}
|
|
674
|
+
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
675
|
+
await markSelfLeft(ctx, guildId);
|
|
676
|
+
try {
|
|
677
|
+
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
678
|
+
} catch (e) {
|
|
679
|
+
console.error(`被禁言自动退群失败 (群号: ${guildId}):`, e);
|
|
680
|
+
quittingGuilds.delete(`${BLACKLIST_PLATFORM}:${guildId}`);
|
|
681
|
+
await clearSelfLeft(ctx, guildId);
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (config.basic.notifyAdminOnMute) {
|
|
686
|
+
const msg = config.basic.muteNotificationMessage.replaceAll("{groupId}", guildId).replaceAll("{groupName}", groupName).replaceAll("{operatorId}", operatorId).replaceAll("{duration}", duration.toString());
|
|
687
|
+
await notifyAdmins(session.bot, config, msg);
|
|
688
|
+
}
|
|
569
689
|
});
|
|
570
690
|
}
|
|
571
691
|
if (config.basic.quitCommandEnabled) {
|
|
@@ -588,7 +708,7 @@ function apply2(ctx, config) {
|
|
|
588
708
|
quittingGuilds.set(`${BLACKLIST_PLATFORM}:${guildId}`, Date.now());
|
|
589
709
|
await markSelfLeft(ctx, guildId);
|
|
590
710
|
try {
|
|
591
|
-
await session.bot.sendMessage(session.guildId, config.basic.quitMessage.
|
|
711
|
+
await session.bot.sendMessage(session.guildId, config.basic.quitMessage.replaceAll("{userId}", userId), platform);
|
|
592
712
|
} catch (e) {
|
|
593
713
|
}
|
|
594
714
|
try {
|
|
@@ -616,9 +736,9 @@ function apply3(ctx, config) {
|
|
|
616
736
|
setInterval(async () => {
|
|
617
737
|
const expireMs = config.invite.inviteExpireDays * 24 * 60 * 60 * 1e3;
|
|
618
738
|
try {
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
console.log(
|
|
739
|
+
await clearExpiredPendingInvites(ctx, expireMs);
|
|
740
|
+
if (config.invite.showDetailedLog) {
|
|
741
|
+
console.log("已执行过期邀请清理");
|
|
622
742
|
}
|
|
623
743
|
} catch (error) {
|
|
624
744
|
console.error("清理过期邀请失败:", error);
|
|
@@ -629,6 +749,8 @@ function apply3(ctx, config) {
|
|
|
629
749
|
ctx.logger("group-control-invite").info(`[guild-request] 触发!userId=${session.userId}, guildId=${session.guildId}, messageId=${session.messageId}, type=${session.type}, subtype=${session.subtype}`);
|
|
630
750
|
ctx.logger("group-control-invite").info(`[guild-request] event 对象: ${JSON.stringify(session.event, null, 2)}`);
|
|
631
751
|
const raw = session.original || session.raw || session.event?._data || {};
|
|
752
|
+
const subType = String(raw.sub_type ?? session.subtype ?? "");
|
|
753
|
+
if (subType && subType !== "invite") return;
|
|
632
754
|
const flag = raw.flag || session.flag || session.messageId;
|
|
633
755
|
const rawUserId = raw.user_id ? String(raw.user_id) : session.userId;
|
|
634
756
|
const rawGroupId = raw.group_id ? String(raw.group_id) : session.guildId;
|
|
@@ -683,7 +805,7 @@ function apply3(ctx, config) {
|
|
|
683
805
|
if (config.invite.autoApprove) {
|
|
684
806
|
try {
|
|
685
807
|
await session.bot.internal.setGroupAddRequest(flag, "invite", true, "");
|
|
686
|
-
|
|
808
|
+
await markApprovedGuild(ctx, rawGroupId);
|
|
687
809
|
if (config.invite.showDetailedLog) {
|
|
688
810
|
console.log(`自动同意群聊邀请: 群号 ${rawGroupId}, 邀请者 ${rawUserId}`);
|
|
689
811
|
}
|
|
@@ -766,7 +888,7 @@ function apply3(ctx, config) {
|
|
|
766
888
|
}
|
|
767
889
|
try {
|
|
768
890
|
await session.bot.internal.setGroupAddRequest(inviteData.flag, "invite", true, "");
|
|
769
|
-
|
|
891
|
+
await markApprovedGuild(ctx, groupId);
|
|
770
892
|
try {
|
|
771
893
|
await session.bot.sendPrivateMessage(inviteData.userId, `您的群聊邀请已通过管理员审核,机器人已加入群聊。`);
|
|
772
894
|
} catch (error) {
|
|
@@ -802,7 +924,7 @@ function apply3(ctx, config) {
|
|
|
802
924
|
}
|
|
803
925
|
});
|
|
804
926
|
ctx.command("gc.pending", "查看待处理的群聊邀请").action(async ({ session }) => {
|
|
805
|
-
if (!
|
|
927
|
+
if (!hasGlobalPermission(session, config)) {
|
|
806
928
|
return "权限不足,只有管理员可以查看待处理邀请。";
|
|
807
929
|
}
|
|
808
930
|
const allInvites = await getAllPendingInvites(ctx);
|
|
@@ -917,6 +1039,7 @@ __name(isUserInitiatedNonCommand, "isUserInitiatedNonCommand");
|
|
|
917
1039
|
function apply4(ctx, config) {
|
|
918
1040
|
const freq = config.frequency;
|
|
919
1041
|
if (!freq.enabled && !freq.privateEnabled) return;
|
|
1042
|
+
const countedSessions = /* @__PURE__ */ new WeakSet();
|
|
920
1043
|
async function checkFrequency(session, isCommand) {
|
|
921
1044
|
const isPrivate = !session.guildId;
|
|
922
1045
|
const platform = session.platform;
|
|
@@ -1004,12 +1127,14 @@ function apply4(ctx, config) {
|
|
|
1004
1127
|
ctx.on("command/before-execute", async (argv) => {
|
|
1005
1128
|
const session = argv.session;
|
|
1006
1129
|
if (isSystemSession(session)) return;
|
|
1130
|
+
if (countedSessions.has(session)) return;
|
|
1007
1131
|
const allowed = await checkFrequency(session, true);
|
|
1008
1132
|
if (!allowed) throw new Error("Blocked");
|
|
1009
1133
|
});
|
|
1010
1134
|
ctx.middleware(async (session, next) => {
|
|
1011
1135
|
if (isSystemSession(session)) return next();
|
|
1012
1136
|
if (!isUserInitiatedNonCommand(session)) return next();
|
|
1137
|
+
countedSessions.add(session);
|
|
1013
1138
|
const allowed = await checkFrequency(session, false);
|
|
1014
1139
|
if (!allowed) return;
|
|
1015
1140
|
return next();
|
|
@@ -1024,6 +1149,51 @@ __export(commands_exports, {
|
|
|
1024
1149
|
name: () => name5
|
|
1025
1150
|
});
|
|
1026
1151
|
var name5 = "group-control-commands";
|
|
1152
|
+
function chunk(arr, size) {
|
|
1153
|
+
const out = [];
|
|
1154
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
__name(chunk, "chunk");
|
|
1158
|
+
async function sendAsForward(session, title, lines) {
|
|
1159
|
+
const groups = chunk(lines, 80);
|
|
1160
|
+
const botId = String(session.bot?.selfId || "10000");
|
|
1161
|
+
const nodes = groups.map((g) => ({
|
|
1162
|
+
type: "node",
|
|
1163
|
+
data: { user_id: botId, nickname: title, content: g.join("\n") }
|
|
1164
|
+
}));
|
|
1165
|
+
try {
|
|
1166
|
+
const internal = session.bot.internal;
|
|
1167
|
+
if (session.guildId) {
|
|
1168
|
+
if (typeof internal.sendGroupForwardMsg !== "function") throw new Error("no forward api");
|
|
1169
|
+
await internal.sendGroupForwardMsg(parseInt(session.guildId), nodes);
|
|
1170
|
+
} else {
|
|
1171
|
+
if (typeof internal.sendPrivateForwardMsg !== "function") throw new Error("no forward api");
|
|
1172
|
+
await internal.sendPrivateForwardMsg(parseInt(session.userId), nodes);
|
|
1173
|
+
}
|
|
1174
|
+
} catch {
|
|
1175
|
+
for (const g of groups) {
|
|
1176
|
+
try {
|
|
1177
|
+
await session.send(g.join("\n"));
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
__name(sendAsForward, "sendAsForward");
|
|
1184
|
+
async function deleteFriendCompat(bot, userId) {
|
|
1185
|
+
const internal = bot.internal;
|
|
1186
|
+
if (typeof internal?.deleteFriend !== "function") {
|
|
1187
|
+
throw new Error("当前适配器不支持 delete_friend 接口");
|
|
1188
|
+
}
|
|
1189
|
+
const n = parseInt(userId);
|
|
1190
|
+
try {
|
|
1191
|
+
await internal.deleteFriend(n);
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
await internal.deleteFriend({ user_id: n, friend_id: n, temp_block: false, both_del: false });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
__name(deleteFriendCompat, "deleteFriendCompat");
|
|
1027
1197
|
function apply5(ctx, config) {
|
|
1028
1198
|
ctx.command("gc", "群控管理员指令");
|
|
1029
1199
|
ctx.command("gc.ban <groupId:text>", "添加群聊到黑名单").action(async ({ session }, input) => {
|
|
@@ -1088,6 +1258,69 @@ function apply5(ctx, config) {
|
|
|
1088
1258
|
if (records.length === 0) return "小群白名单为空。";
|
|
1089
1259
|
return "小群白名单列表(以下群不受小群人数限制):\n" + records.map((r) => `- ${r.guildId}`).join("\n");
|
|
1090
1260
|
});
|
|
1261
|
+
ctx.command("gc.friends", "列出机器人的好友(合并转发)").action(async ({ session }) => {
|
|
1262
|
+
if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
|
|
1263
|
+
let list = [];
|
|
1264
|
+
try {
|
|
1265
|
+
const raw = await session.bot.internal.getFriendList();
|
|
1266
|
+
list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
return `获取好友列表失败:${e.message}`;
|
|
1269
|
+
}
|
|
1270
|
+
if (list.length === 0) return "好友列表为空。";
|
|
1271
|
+
const lines = list.map((f, i) => {
|
|
1272
|
+
const uid = f.user_id ?? f.userId ?? "";
|
|
1273
|
+
const name9 = f.remark || f.nickname || f.nick || String(uid);
|
|
1274
|
+
return `${i + 1}. ${name9} (${uid})`;
|
|
1275
|
+
});
|
|
1276
|
+
await sendAsForward(session, `好友列表(共 ${list.length} 个)`, lines);
|
|
1277
|
+
return "";
|
|
1278
|
+
});
|
|
1279
|
+
ctx.command("gc.delfriend <userId:text>", "删除指定好友").action(async ({ session }, input) => {
|
|
1280
|
+
if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
|
|
1281
|
+
const userId = (input || "").trim();
|
|
1282
|
+
if (!/^\d+$/.test(userId)) return "输入格式错误,请输入要删除的好友 QQ 号。";
|
|
1283
|
+
try {
|
|
1284
|
+
await deleteFriendCompat(session.bot, userId);
|
|
1285
|
+
return `已删除好友 ${userId}。`;
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
return `删除好友失败:${e.message}`;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
ctx.command("gc.groups", "列出机器人所在的群(合并转发)").action(async ({ session }) => {
|
|
1291
|
+
if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
|
|
1292
|
+
let list = [];
|
|
1293
|
+
try {
|
|
1294
|
+
const raw = await session.bot.internal.getGroupList();
|
|
1295
|
+
list = Array.isArray(raw) ? raw : Array.isArray(raw?.data) ? raw.data : [];
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
return `获取群列表失败:${e.message}`;
|
|
1298
|
+
}
|
|
1299
|
+
if (list.length === 0) return "机器人尚未加入任何群。";
|
|
1300
|
+
const lines = list.map((g, i) => {
|
|
1301
|
+
const gid = g.group_id ?? g.groupId ?? "";
|
|
1302
|
+
const gname = g.group_name ?? g.groupName ?? String(gid);
|
|
1303
|
+
const count = g.member_count ?? g.memberCount;
|
|
1304
|
+
const max = g.max_member_count ?? g.maxMemberCount;
|
|
1305
|
+
const sizeInfo = count != null ? `(${count}${max != null ? `/${max}` : ""}人)` : "";
|
|
1306
|
+
return `${i + 1}. ${gname} (${gid})${sizeInfo}`;
|
|
1307
|
+
});
|
|
1308
|
+
await sendAsForward(session, `群列表(共 ${list.length} 个)`, lines);
|
|
1309
|
+
return "";
|
|
1310
|
+
});
|
|
1311
|
+
ctx.command("gc.leave <groupId:text>", "让机器人退出指定群").action(async ({ session }, input) => {
|
|
1312
|
+
if (!hasGlobalPermission(session, config)) return "权限不足,只有全局管理员可以执行此操作。";
|
|
1313
|
+
const guildId = parseGuildId(input);
|
|
1314
|
+
if (!guildId) return "输入格式错误,请输入要退出的群号。";
|
|
1315
|
+
await markSelfLeft(ctx, guildId);
|
|
1316
|
+
try {
|
|
1317
|
+
await session.bot.internal.setGroupLeave(parseInt(guildId));
|
|
1318
|
+
return `已退出群 ${guildId}。`;
|
|
1319
|
+
} catch (e) {
|
|
1320
|
+
await clearSelfLeft(ctx, guildId);
|
|
1321
|
+
return `退出群 ${guildId} 失败:${e.message}`;
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1091
1324
|
}
|
|
1092
1325
|
__name(apply5, "apply");
|
|
1093
1326
|
|
|
@@ -1281,7 +1514,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1281
1514
|
import_koishi.Schema.const("koishi").description("使用 Koishi 自带权限系统 (authority)"),
|
|
1282
1515
|
import_koishi.Schema.const("builtin").description("使用插件内置权限管理 (群管理员/群主)")
|
|
1283
1516
|
]).default("builtin").description("权限管理模式"),
|
|
1284
|
-
koishiAuthority: import_koishi.Schema.
|
|
1517
|
+
koishiAuthority: import_koishi.Schema.natural().default(3).description("Koishi 模式下管理指令所需的最低权限等级"),
|
|
1285
1518
|
protectedCommands: import_koishi.Schema.array(String).default([]).description("需要群管理员权限才能使用的自定义指令名列表")
|
|
1286
1519
|
}).description("权限管理")
|
|
1287
1520
|
}),
|
|
@@ -1291,18 +1524,24 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1291
1524
|
quitCommandEnabled: import_koishi.Schema.boolean().default(true).description("启用 quit 指令"),
|
|
1292
1525
|
quitMessage: import_koishi.Schema.string().default("收到来自{userId}的指令,即将退出群聊。").description("quit 指令触发后的群内提示,支持变量 {userId}"),
|
|
1293
1526
|
enableBlacklist: import_koishi.Schema.boolean().default(true).description("启用被踢出自动拉黑"),
|
|
1294
|
-
blacklistMessage: import_koishi.Schema.string().default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后的提示"),
|
|
1527
|
+
blacklistMessage: import_koishi.Schema.string().role("textarea").default("此群聊已被拉黑,机器人将自动退出,请联系管理员移出黑名单。").description("被拉入黑名单群后的提示"),
|
|
1295
1528
|
notifyAdminOnKick: import_koishi.Schema.boolean().default(true).description("被踢出群时通知管理员"),
|
|
1296
|
-
kickNotificationMessage: import_koishi.Schema.string().default("机器人已被踢出群聊\n群名称:{groupName}\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知模板,支持变量 {groupId}, {groupName}"),
|
|
1529
|
+
kickNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人已被踢出群聊\n群名称:{groupName}\n群号:{groupId}\n该群已被自动加入黑名单。").description("被踢出群通知模板,支持变量 {groupId}, {groupName}"),
|
|
1297
1530
|
smallGroupAutoQuit: import_koishi.Schema.boolean().default(false).description("启用小群自动退群"),
|
|
1298
|
-
smallGroupThreshold: import_koishi.Schema.
|
|
1299
|
-
smallGroupCheckDelay: import_koishi.Schema.
|
|
1300
|
-
|
|
1531
|
+
smallGroupThreshold: import_koishi.Schema.natural().default(30).description("小群人数阈值(低于等于此值时自动退群)"),
|
|
1532
|
+
smallGroupCheckDelay: import_koishi.Schema.natural().default(3e3).description("加入后延迟检测时间(毫秒)"),
|
|
1533
|
+
smallGroupExcludeOfficialBots: import_koishi.Schema.boolean().default(true).description("统计群人数时排除 QQ 官方机器人(is_robot)及机器人自身,仅统计真人成员"),
|
|
1534
|
+
smallGroupRealtimeMonitor: import_koishi.Schema.boolean().default(true).description("实时监控群人数(监听成员退群事件,群缩小到阈值以下时自动退群)"),
|
|
1535
|
+
smallGroupRecheckCooldown: import_koishi.Schema.natural().default(60).description("实时监控:同一群两次复检的最小间隔(秒),避免成员批量退群时频繁调用接口"),
|
|
1536
|
+
smallGroupQuitMessage: import_koishi.Schema.string().role("textarea").default("该群人数过少({memberCount}人),不满足最低人数要求({threshold}人),机器人将自动退出。").description("小群退群提示,支持变量 {memberCount}, {threshold}, {groupName}, {groupId}"),
|
|
1301
1537
|
smallGroupNotifyAdmin: import_koishi.Schema.boolean().default(true).description("小群自动退群时通知管理员"),
|
|
1302
1538
|
smallGroupQualifiedNotifyAdmin: import_koishi.Schema.boolean().default(true).description("未经审核被拉入人数达标的群时通知管理员"),
|
|
1303
|
-
smallGroupQualifiedMessage: import_koishi.Schema.string().default("机器人被未经审核地拉入群聊\n群名称:{groupName}\n群号:{groupId}\n当前人数:{memberCount}人(阈值:{threshold}人)\n请确认是否保留。").description("合格小群通知模板,支持变量 {groupName}, {groupId}, {memberCount}, {threshold}"),
|
|
1539
|
+
smallGroupQualifiedMessage: import_koishi.Schema.string().role("textarea").default("机器人被未经审核地拉入群聊\n群名称:{groupName}\n群号:{groupId}\n当前人数:{memberCount}人(阈值:{threshold}人)\n请确认是否保留。").description("合格小群通知模板,支持变量 {groupName}, {groupId}, {memberCount}, {threshold}"),
|
|
1304
1540
|
notifyAdminOnMute: import_koishi.Schema.boolean().default(false).description("机器人被禁言时通知管理员"),
|
|
1305
|
-
muteNotificationMessage: import_koishi.Schema.string().default("机器人在群聊中被禁言\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}")
|
|
1541
|
+
muteNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人在群聊中被禁言\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}"),
|
|
1542
|
+
muteAutoQuit: import_koishi.Schema.boolean().default(false).description("机器人被禁言达到阈值时自动退群并拉黑"),
|
|
1543
|
+
muteAutoQuitThreshold: import_koishi.Schema.natural().default(600).description("触发自动退群的禁言时长阈值(秒),被禁言时长 ≥ 此值即退群并拉黑"),
|
|
1544
|
+
muteQuitNotificationMessage: import_koishi.Schema.string().role("textarea").default("机器人被长时间禁言,已自动退群并拉黑\n群名称:{groupName}\n群号:{groupId}\n操作者:{operatorId}\n禁言时长:{duration}秒").description("被禁言自动退群时发给管理员的通知模板,支持变量 {groupId}, {groupName}, {operatorId}, {duration}")
|
|
1306
1545
|
}).description("基础群组管理")
|
|
1307
1546
|
}),
|
|
1308
1547
|
import_koishi.Schema.object({
|
|
@@ -1310,11 +1549,11 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1310
1549
|
enabled: import_koishi.Schema.boolean().default(false).description("启用群聊邀请审核"),
|
|
1311
1550
|
autoApprove: import_koishi.Schema.boolean().default(false).description("自动同意邀请"),
|
|
1312
1551
|
notifyAdminOnApprove: import_koishi.Schema.boolean().default(true).description("自动同意时是否仍通知管理员"),
|
|
1313
|
-
inviteWaitMessage: import_koishi.Schema.string().default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发给邀请者的等待提示"),
|
|
1314
|
-
inviteApproveMessage: import_koishi.Schema.string().default("已自动通过您的群聊邀请,机器人正在加入群聊。").description("自动同意时发给邀请者的提示,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1315
|
-
inviteRequestMessage: import_koishi.Schema.string().default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请使用指令 gc.approve {groupId} 同意或 gc.reject {groupId} 拒绝。").description("发给管理员的请求消息模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1316
|
-
inviteApproveNotificationMessage: import_koishi.Schema.string().default("已自动通过群聊邀请\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})").description("自动同意时发给管理员的通知模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1317
|
-
inviteExpireDays: import_koishi.Schema.
|
|
1552
|
+
inviteWaitMessage: import_koishi.Schema.string().role("textarea").default("已收到您的群聊邀请,正在等待管理员审核,请耐心等待。").description("发给邀请者的等待提示"),
|
|
1553
|
+
inviteApproveMessage: import_koishi.Schema.string().role("textarea").default("已自动通过您的群聊邀请,机器人正在加入群聊。").description("自动同意时发给邀请者的提示,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1554
|
+
inviteRequestMessage: import_koishi.Schema.string().role("textarea").default("收到新的群聊邀请请求:\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})\n\n请使用指令 gc.approve {groupId} 同意或 gc.reject {groupId} 拒绝。").description("发给管理员的请求消息模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1555
|
+
inviteApproveNotificationMessage: import_koishi.Schema.string().role("textarea").default("已自动通过群聊邀请\n群名称:{groupName}\n群号:{groupId}\n邀请者:{userName} (QQ: {userId})").description("自动同意时发给管理员的通知模板,支持变量 {groupName}, {groupId}, {userName}, {userId}"),
|
|
1556
|
+
inviteExpireDays: import_koishi.Schema.natural().default(3).description("邀请记录过期天数"),
|
|
1318
1557
|
showDetailedLog: import_koishi.Schema.boolean().default(false).description("显示详细日志")
|
|
1319
1558
|
}).description("群聊邀请审核")
|
|
1320
1559
|
}),
|
|
@@ -1323,28 +1562,28 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1323
1562
|
enabled: import_koishi.Schema.boolean().default(false).description("启用好友申请管理"),
|
|
1324
1563
|
autoApprove: import_koishi.Schema.boolean().default(false).description("自动通过好友申请(否则通知管理员手动处理)"),
|
|
1325
1564
|
notifyAdminOnApprove: import_koishi.Schema.boolean().default(true).description("自动通过时是否仍通知管理员"),
|
|
1326
|
-
requestExpireDays: import_koishi.Schema.
|
|
1327
|
-
requestMessage: import_koishi.Schema.string().default("收到新的好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}\n\n使用 gc.fa {userId} 同意或 gc.fr {userId} 拒绝。").description("通知管理员的消息模板,支持变量 {userId}, {nickname}, {comment}"),
|
|
1328
|
-
approveNotificationMessage: import_koishi.Schema.string().default("已自动通过好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}").description("自动通过时的通知模板,支持变量 {userId}, {nickname}, {comment}")
|
|
1565
|
+
requestExpireDays: import_koishi.Schema.natural().default(7).description("待处理申请的过期天数"),
|
|
1566
|
+
requestMessage: import_koishi.Schema.string().role("textarea").default("收到新的好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}\n\n使用 gc.fa {userId} 同意或 gc.fr {userId} 拒绝。").description("通知管理员的消息模板,支持变量 {userId}, {nickname}, {comment}"),
|
|
1567
|
+
approveNotificationMessage: import_koishi.Schema.string().role("textarea").default("已自动通过好友申请\nQQ:{userId}\n昵称:{nickname}\n附言:{comment}").description("自动通过时的通知模板,支持变量 {userId}, {nickname}, {comment}")
|
|
1329
1568
|
}).description("好友申请管理")
|
|
1330
1569
|
}),
|
|
1331
1570
|
import_koishi.Schema.object({
|
|
1332
1571
|
frequency: import_koishi.Schema.object({
|
|
1333
1572
|
enabled: import_koishi.Schema.boolean().default(false).description("启用群聊频率控制(指令及 @ 对话均受限)"),
|
|
1334
|
-
limit: import_koishi.Schema.
|
|
1335
|
-
window: import_koishi.Schema.
|
|
1336
|
-
warnDelay: import_koishi.Schema.
|
|
1337
|
-
blockDur: import_koishi.Schema.
|
|
1573
|
+
limit: import_koishi.Schema.natural().default(5).description("群聊:时间窗口内允许的最大触发次数"),
|
|
1574
|
+
window: import_koishi.Schema.natural().default(60).description("群聊:时间窗口(秒)"),
|
|
1575
|
+
warnDelay: import_koishi.Schema.natural().default(30).description("群聊:警告后再次触发的时间阈值(秒),超出则进入屏蔽"),
|
|
1576
|
+
blockDur: import_koishi.Schema.natural().default(300).description("群聊:首次屏蔽的基础时长(秒)"),
|
|
1338
1577
|
whitelist: import_koishi.Schema.array(String).default([]).description("群聊:不受频率限制的群号列表"),
|
|
1339
1578
|
privateEnabled: import_koishi.Schema.boolean().default(false).description("启用私聊频率控制"),
|
|
1340
|
-
privateLimit: import_koishi.Schema.
|
|
1341
|
-
privateWindow: import_koishi.Schema.
|
|
1342
|
-
privateWarnDelay: import_koishi.Schema.
|
|
1343
|
-
privateBlockDur: import_koishi.Schema.
|
|
1579
|
+
privateLimit: import_koishi.Schema.natural().default(10).description("私聊:时间窗口内允许的最大触发次数"),
|
|
1580
|
+
privateWindow: import_koishi.Schema.natural().default(60).description("私聊:时间窗口(秒)"),
|
|
1581
|
+
privateWarnDelay: import_koishi.Schema.natural().default(30).description("私聊:警告后再次触发的时间阈值(秒)"),
|
|
1582
|
+
privateBlockDur: import_koishi.Schema.natural().default(300).description("私聊:首次屏蔽的基础时长(秒)"),
|
|
1344
1583
|
privateWhitelist: import_koishi.Schema.array(String).default([]).description("私聊:不受频率限制的用户ID列表"),
|
|
1345
|
-
blockExpBase: import_koishi.Schema.
|
|
1346
|
-
blockExpWindow: import_koishi.Schema.
|
|
1347
|
-
blockNotifyCooldown: import_koishi.Schema.
|
|
1584
|
+
blockExpBase: import_koishi.Schema.natural().min(1).default(2).description("屏蔽时长指数增长底数(时长 = blockDur × base^(次数-1)),设为 1 禁用"),
|
|
1585
|
+
blockExpWindow: import_koishi.Schema.natural().default(3600).description("指数增长重置窗口(秒),从最后一次屏蔽结束计算,超出则重置次数"),
|
|
1586
|
+
blockNotifyCooldown: import_koishi.Schema.natural().default(60).description("屏蔽期间提示消息的冷却时间(秒),避免刷屏"),
|
|
1348
1587
|
warnMsg: import_koishi.Schema.string().default("发言频率过高,请慢一点~").description("首次超限警告消息"),
|
|
1349
1588
|
blockMsg: import_koishi.Schema.string().default("发言频率过高,已被禁用 {duration} 秒。").description("进入屏蔽时的通知,支持变量 {duration}"),
|
|
1350
1589
|
blockedMsg: import_koishi.Schema.string().default("暂时被禁用,还有 {time} 秒解禁。").description("屏蔽期间再次触发时的提示,支持变量 {time}")
|
|
@@ -1354,13 +1593,14 @@ var Config = import_koishi.Schema.intersect([
|
|
|
1354
1593
|
botSwitch: import_koishi.Schema.object({
|
|
1355
1594
|
enabled: import_koishi.Schema.boolean().default(true).description("启用群聊 bot 开关"),
|
|
1356
1595
|
defaultState: import_koishi.Schema.boolean().default(true).description("默认开启状态"),
|
|
1357
|
-
disabledMessage: import_koishi.Schema.string().default("机器人当前在此群处于关闭状态,请使用 bot-on 开启。").description("关闭状态下被 @ 时的提示")
|
|
1596
|
+
disabledMessage: import_koishi.Schema.string().role("textarea").default("机器人当前在此群处于关闭状态,请使用 bot-on 开启。").description("关闭状态下被 @ 时的提示")
|
|
1358
1597
|
}).description("机器人开关控制")
|
|
1359
1598
|
})
|
|
1360
1599
|
]);
|
|
1361
1600
|
|
|
1362
1601
|
// src/index.ts
|
|
1363
1602
|
var name8 = "group-control";
|
|
1603
|
+
var inject = ["database"];
|
|
1364
1604
|
function apply8(ctx, config) {
|
|
1365
1605
|
ctx.plugin(database_exports);
|
|
1366
1606
|
ctx.plugin(basic_exports, config);
|
|
@@ -1375,5 +1615,6 @@ __name(apply8, "apply");
|
|
|
1375
1615
|
0 && (module.exports = {
|
|
1376
1616
|
Config,
|
|
1377
1617
|
apply,
|
|
1618
|
+
inject,
|
|
1378
1619
|
name
|
|
1379
1620
|
});
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Session } from 'koishi';
|
|
2
2
|
import { Config } from './config';
|
|
3
3
|
export declare function isBlacklistEnabled(config: Config['basic']): string | null;
|
|
4
|
-
export declare function parseGuildId(input: string): string | null;
|
|
4
|
+
export declare function parseGuildId(input: string | undefined | null): string | null;
|
|
5
5
|
export declare function formatDate(timestamp: number): string;
|
|
6
6
|
export declare function notifyAdmins(bot: any, config: Config, message: string): Promise<void>;
|
|
7
7
|
/** 是否为全局管理员(填在 adminQQs 里的) */
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -11,13 +11,15 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
|
11
11
|
## 功能概览
|
|
12
12
|
|
|
13
13
|
- **黑名单管理**:被踢出群后自动拉黑,下次被邀请时自动退出
|
|
14
|
-
-
|
|
14
|
+
- **小群自动退群**:加入人数不足的群时自动退出并通知管理员,统计人数时自动排除 QQ 官方机器人(仅计真人)
|
|
15
|
+
- **实时小群监控**:监听成员退群事件,群缩水到阈值以下时自动退出(仅针对未经审核拉入的群)
|
|
15
16
|
- **合格小群通知**:未经审核被拉入人数达标的群时,通知管理员确认
|
|
16
17
|
- **群聊邀请审核**:收到邀请时暂缓加入,等待管理员审核
|
|
17
18
|
- **好友申请管理**:收到好友申请时通知管理员,或自动通过
|
|
18
19
|
- **频率控制**:限制群聊/私聊的指令及对话频率,支持指数增长屏蔽时长
|
|
19
20
|
- **Bot 开关**:按群独立开关 bot,关闭后屏蔽所有响应
|
|
20
|
-
-
|
|
21
|
+
- **好友/群管理**:指令列出好友(合并转发)、删除好友、列出所在群、远程退出指定群
|
|
22
|
+
- **被禁言通知**:bot 被禁言时可选通知管理员;被禁言时长达到阈值时可自动退群并拉黑
|
|
21
23
|
- **权限管理**:支持 Koishi authority 或内置群管理员两种权限模式
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -69,6 +71,9 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
|
69
71
|
|--------|--------|------|
|
|
70
72
|
| `smallGroupAutoQuit` | `false` | 启用小群自动退群 |
|
|
71
73
|
| `smallGroupThreshold` | `30` | 人数阈值,低于等于此值时自动退出 |
|
|
74
|
+
| `smallGroupExcludeOfficialBots` | `true` | 统计群人数时排除 QQ 官方机器人(`is_robot`)及机器人自身,仅统计真人成员 |
|
|
75
|
+
| `smallGroupRealtimeMonitor` | `true` | 实时监控群人数:监听成员退群事件,群缩小到阈值以下时自动退群(仅监控未经审核拉入的群) |
|
|
76
|
+
| `smallGroupRecheckCooldown` | `60` | 实时监控时同一群两次复检的最小间隔(秒),避免成员批量退群时频繁调用接口 |
|
|
72
77
|
| `smallGroupQuitMessage` | *(见配置)* | 退群提示,支持 `{memberCount}`, `{threshold}`, `{groupName}`, `{groupId}` |
|
|
73
78
|
| `smallGroupNotifyAdmin` | `true` | 自动退群时通知管理员 |
|
|
74
79
|
| `smallGroupCheckDelay` | `3000` | 加入后延迟检测的时间(毫秒) |
|
|
@@ -86,6 +91,9 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
|
86
91
|
|--------|--------|------|
|
|
87
92
|
| `notifyAdminOnMute` | `false` | bot 被禁言时通知管理员 |
|
|
88
93
|
| `muteNotificationMessage` | *(见配置)* | 通知消息模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
|
|
94
|
+
| `muteAutoQuit` | `false` | bot 被禁言达到阈值时自动退群并拉黑 |
|
|
95
|
+
| `muteAutoQuitThreshold` | `600` | 触发自动退群的禁言时长阈值(秒),被禁言时长 ≥ 此值即退群并拉黑 |
|
|
96
|
+
| `muteQuitNotificationMessage` | *(见配置)* | 自动退群时发给管理员的通知模板,支持 `{groupId}`, `{groupName}`, `{operatorId}`, `{duration}` |
|
|
89
97
|
|
|
90
98
|
### 频率控制
|
|
91
99
|
|
|
@@ -174,6 +182,10 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
|
174
182
|
| `gc.sg-add <群号>` | 将群加入小群白名单,不受人数限制 |
|
|
175
183
|
| `gc.sg-rm <群号>` | 从小群白名单移除群 |
|
|
176
184
|
| `gc.sg-list` | 查看小群白名单 |
|
|
185
|
+
| `gc.friends` | 列出机器人的好友(合并转发形式发送) |
|
|
186
|
+
| `gc.delfriend <QQ号>` | 删除指定好友 |
|
|
187
|
+
| `gc.groups` | 列出机器人所在的群(合并转发形式发送) |
|
|
188
|
+
| `gc.leave <群号>` | 让机器人退出指定群 |
|
|
177
189
|
| `gc.approve <群号>` | 同意加入指定群 |
|
|
178
190
|
| `gc.reject <群号>` | 拒绝加入指定群 |
|
|
179
191
|
| `gc.pending` | 查看待处理的群聊邀请列表 |
|
|
@@ -197,3 +209,5 @@ Koishi 插件,多功能群聊自管理工具。仅支持 OneBot 适配器。
|
|
|
197
209
|
- 管理员通知依赖 `admin.adminQQs` 或 `admin.notificationGroupId` 的配置,未配置则无法收到通知
|
|
198
210
|
- 频率控制的非指令拦截(@ 对话、私聊)不影响入群欢迎等系统事件
|
|
199
211
|
- 小群合格通知仅在启用了 `smallGroupAutoQuit` 且未经 `gc.approve` 审核通过的情况下触发
|
|
212
|
+
- **小群人数统计**:开启 `smallGroupExcludeOfficialBots` 后只计真人成员(排除 `is_robot` 机器人与自身)。检测做了分级短路以减少接口调用——原始人数 ≤ 阈值直接退群、原始人数 > 阈值 + 20(单群机器人上限)直接保留,仅当人数处于中间区间时才拉取一次成员列表,且统计到足够机器人即提前结束遍历
|
|
213
|
+
- **实时小群监控**:纯事件驱动(仅监听成员退群),不做轮询,配合 per-群冷却限流,几乎不增加接口压力。**经 `gc.approve` 审核通过或在小群白名单中的群永久豁免**,仅监控未经审核被拉入的群;机器人退出某群后其审核标记自动清除,若日后被未经审核地重新拉入会重新接受检测
|