koishi-plugin-blacklist-online 0.1.3 → 0.1.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/LICENSE +21 -0
- package/lib/core.d.ts +2 -1
- package/lib/core.js +54 -12
- package/lib/index.d.ts +1 -1
- package/lib/index.js +31 -19
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 furryaxw
|
|
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/core.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Context, Session } from 'koishi';
|
|
|
2
2
|
import { PluginConfig } from './types';
|
|
3
3
|
export declare const sleep: (ms: number) => Promise<unknown>;
|
|
4
4
|
export declare function isUserAdmin(session: Session, config: PluginConfig, userId: string): Promise<boolean>;
|
|
5
|
-
export declare function syncBlacklist(ctx: Context, config: PluginConfig): Promise<
|
|
5
|
+
export declare function syncBlacklist(ctx: Context, config: PluginConfig): Promise<boolean>;
|
|
6
6
|
export declare function queueRequest(ctx: Context, type: 'ADD' | 'REMOVE' | 'CANCEL', payload: any): Promise<any>;
|
|
7
7
|
export declare function processOfflineQueue(ctx: Context, config: PluginConfig): Promise<void>;
|
|
8
8
|
export declare function checkAndHandleUser(ctx: Context, config: PluginConfig, session: Session, user_id: string): Promise<boolean>;
|
|
@@ -13,3 +13,4 @@ guildId: string): Promise<{
|
|
|
13
13
|
total: number;
|
|
14
14
|
error?: string;
|
|
15
15
|
}>;
|
|
16
|
+
export declare function scanAllGuilds(ctx: Context, config: PluginConfig): Promise<void>;
|
package/lib/core.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.processOfflineQueue = processOfflineQueue;
|
|
|
8
8
|
exports.checkAndHandleUser = checkAndHandleUser;
|
|
9
9
|
exports.parseUserId = parseUserId;
|
|
10
10
|
exports.scanGuild = scanGuild;
|
|
11
|
+
exports.scanAllGuilds = scanAllGuilds;
|
|
11
12
|
const koishi_1 = require("koishi");
|
|
12
13
|
const node_crypto_1 = require("node:crypto");
|
|
13
14
|
const logger = new koishi_1.Logger('blacklist-online');
|
|
@@ -50,36 +51,44 @@ async function syncBlacklist(ctx, config) {
|
|
|
50
51
|
timeout: 10000
|
|
51
52
|
});
|
|
52
53
|
const { strategy, newRevision, data } = response;
|
|
54
|
+
let hasNewEntries = false; // 标记是否有新增
|
|
53
55
|
if (strategy === 'up-to-date') {
|
|
54
56
|
logger.debug('✅ 黑名单已是最新');
|
|
55
|
-
return;
|
|
57
|
+
return false;
|
|
56
58
|
}
|
|
57
59
|
if (strategy === 'full_replace') {
|
|
58
|
-
logger.info(
|
|
60
|
+
logger.info(`执行全量同步,条数: ${data.length}`);
|
|
61
|
+
// 1. 先清空本地表,确保环境干净
|
|
59
62
|
await ctx.database.remove('blacklist_users', {});
|
|
63
|
+
// 2. 批量写入。由于我们定义了 primary: 'user_id',这里即便有重复也会直接覆盖
|
|
60
64
|
if (data.length > 0) {
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const batchSize = 50;
|
|
65
|
+
// 分批处理,防止 SQLite 单次 SQL 语句过长
|
|
66
|
+
const batchSize = 100;
|
|
64
67
|
for (let i = 0; i < data.length; i += batchSize) {
|
|
65
68
|
const batch = data.slice(i, i + batchSize);
|
|
66
|
-
// 这里的字段名必须与后端返回的 snake_case 保持完全一致
|
|
67
69
|
await ctx.database.upsert('blacklist_users', batch);
|
|
68
70
|
}
|
|
71
|
+
hasNewEntries = true; // 全量更新通常视为有变化,触发一次扫描比较安全
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
else if (strategy === 'incremental') {
|
|
72
75
|
logger.info(`📥 增量同步 -> ${newRevision}`);
|
|
73
|
-
if (data.upserts?.length)
|
|
76
|
+
if (data.upserts?.length) {
|
|
74
77
|
await ctx.database.upsert('blacklist_users', data.upserts);
|
|
75
|
-
|
|
78
|
+
hasNewEntries = true; // 有新增或更新,标记为 true
|
|
79
|
+
}
|
|
80
|
+
// 删除操作不触发扫描
|
|
81
|
+
if (data.deletes?.length) {
|
|
76
82
|
await ctx.database.remove('blacklist_users', { user_id: data.deletes });
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
await ctx.database.upsert('blacklist_meta', [{ key: 'sync_revision', value: newRevision }]);
|
|
79
86
|
logger.info('✅ 同步完成');
|
|
87
|
+
return hasNewEntries;
|
|
80
88
|
}
|
|
81
89
|
catch (error) {
|
|
82
90
|
logger.warn(`❌ 同步失败: ${error.message || error}`);
|
|
91
|
+
return false;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
// --- 3. 队列入队 ---
|
|
@@ -190,8 +199,14 @@ async function checkAndHandleUser(ctx, config, session, user_id) {
|
|
|
190
199
|
.replace('{userId}', user_id)
|
|
191
200
|
.replace('{reason}', reason)
|
|
192
201
|
.replace('{guild}', session.guildId);
|
|
193
|
-
//
|
|
194
|
-
|
|
202
|
+
// 发送消息
|
|
203
|
+
try {
|
|
204
|
+
// 扫描时 session 是伪造的,没有 messageId,直接 send 即可
|
|
205
|
+
await session.send(session.messageId ? (0, koishi_1.h)('quote', { id: session.messageId }) + msg : msg);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
logger.warn(`[群: ${session.guildId}] 发送通知失败: ${e}`);
|
|
209
|
+
}
|
|
195
210
|
}
|
|
196
211
|
// 执行踢出
|
|
197
212
|
let kicked = false;
|
|
@@ -208,7 +223,10 @@ async function checkAndHandleUser(ctx, config, session, user_id) {
|
|
|
208
223
|
await (0, exports.sleep)(config.retryDelay);
|
|
209
224
|
else {
|
|
210
225
|
const failMsg = config.kickFailMessage.replace('{user}', displayName).replace('{reason}', String(e));
|
|
211
|
-
|
|
226
|
+
try {
|
|
227
|
+
await session.send(failMsg);
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
}
|
|
@@ -262,7 +280,8 @@ guildId) {
|
|
|
262
280
|
const fakeSession = bot.session({
|
|
263
281
|
type: 'message',
|
|
264
282
|
guildId,
|
|
265
|
-
|
|
283
|
+
channelId: guildId,
|
|
284
|
+
user: { id: bot.selfId },
|
|
266
285
|
});
|
|
267
286
|
let handled = 0;
|
|
268
287
|
const BATCH_SIZE = 5;
|
|
@@ -284,3 +303,26 @@ guildId) {
|
|
|
284
303
|
return { handled: 0, total: 0, error: String(error) };
|
|
285
304
|
}
|
|
286
305
|
}
|
|
306
|
+
async function scanAllGuilds(ctx, config) {
|
|
307
|
+
logger.info('🚀 检测到黑名单更新,触发自动全局扫描...');
|
|
308
|
+
let totalHandled = 0;
|
|
309
|
+
let processedGuilds = 0;
|
|
310
|
+
for (const bot of ctx.bots) {
|
|
311
|
+
try {
|
|
312
|
+
const guilds = await bot.getGuildList();
|
|
313
|
+
for (const guild of guilds.data) {
|
|
314
|
+
// 调用现有的单群扫描逻辑
|
|
315
|
+
const result = await scanGuild(ctx, config, bot, guild.id);
|
|
316
|
+
if (result.handled > 0) {
|
|
317
|
+
logger.info(`[自动扫描] 群 ${guild.id}: 处理 ${result.handled} 人`);
|
|
318
|
+
totalHandled += result.handled;
|
|
319
|
+
}
|
|
320
|
+
processedGuilds++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
logger.warn(`Bot ${bot.selfId} 自动扫描出错: ${e}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
logger.info(`✅ 自动扫描完成。扫描群组: ${processedGuilds}, 处理人数: ${totalHandled}`);
|
|
328
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { Context, Schema } from 'koishi';
|
|
|
2
2
|
import { PluginConfig } from './types';
|
|
3
3
|
export declare const name = "blacklist-online";
|
|
4
4
|
export declare const inject: string[];
|
|
5
|
-
export declare const usage = "\n## \u529F\u80FD\u8BF4\u660E\n\u4E00\u4E2A\u5F3A\u5927\u7684\u3001\u57FA\u4E8E\u6570\u636E\u5E93\u7684\u7FA4\u7EC4\u9ED1\u540D\u5355\u7BA1\u7406\u63D2\u4EF6\u3002\n- **\u53D7\u4FDD\u62A4\u540D\u5355**: \u914D\u7F6E\u4E2D\u7684\u7528\u6237\u65E0\u6CD5\u88AB\u62C9\u9ED1\u3002\n- **\u5206\u7FA4\u7BA1\u7406**: \u53EF\u4E3A\u6BCF\u4E2A\u7FA4\u72EC\u7ACB\u8BBE\u7F6E\
|
|
5
|
+
export declare const usage = "\n## \u529F\u80FD\u8BF4\u660E\n\u4E00\u4E2A\u5F3A\u5927\u7684\u3001\u57FA\u4E8E\u6570\u636E\u5E93\u7684\u7FA4\u7EC4\u9ED1\u540D\u5355\u7BA1\u7406\u63D2\u4EF6\u3002\n\n- **\u4E91\u7AEF\u540C\u6B65 & \u81EA\u52A8\u5DE1\u822A**: \u5B9E\u65F6\u540C\u6B65\u4E91\u7AEF\u9ED1\u540D\u5355\u3002**\u5F53\u68C0\u6D4B\u5230\u65B0\u589E\u9ED1\u540D\u5355\u7528\u6237\u65F6\uFF0C\u4F1A\u81EA\u52A8\u89E6\u53D1\u5168\u5C40\u626B\u63CF**\uFF0C\u53CA\u65F6\u6E05\u7406\u6240\u6709\u7FA4\u5185\u7684\u6F5C\u5728\u5A01\u80C1\u3002\n- **\u516C\u5F00\u5904\u5211**: \u65E0\u8BBA\u662F\u81EA\u52A8\u626B\u63CF\u8FD8\u662F\u624B\u52A8\u626B\u63CF\uFF0C\u53D1\u73B0\u9ED1\u540D\u5355\u7528\u6237\u65F6\u5747\u4F1A\u5728\u7FA4\u5185\u53D1\u9001\u5305\u542B\u539F\u56E0\u7684\u901A\u544A\uFF08\u53D6\u51B3\u4E8E\u7FA4\u6A21\u5F0F\u8BBE\u7F6E\uFF09\u3002\n- **\u53D7\u4FDD\u62A4\u540D\u5355**: \u914D\u7F6E\u4E2D\u7684\u7528\u6237\uFF08\u5982\u7FA4\u4E3B\u3001\u7279\u5B9A\u7BA1\u7406\u5458\uFF09\u62E5\u6709\u8C41\u514D\u6743\uFF0C\u65E0\u6CD5\u88AB\u62C9\u9ED1\u3002\n- **\u5206\u7FA4\u7BA1\u7406**: \u53EF\u4E3A\u6BCF\u4E2A\u7FA4\u72EC\u7ACB\u8BBE\u7F6E\u5904\u7406\u6A21\u5F0F\uFF08\u4EC5\u901A\u77E5 / \u4EC5\u8E22\u51FA / \u901A\u77E5\u5E76\u8E22\u51FA / \u5173\u95ED\uFF09\u3002\n- **\u5165\u7FA4\u68C0\u6D4B**: \u65B0\u6210\u5458\u8FDB\u7FA4\u6216\u7533\u8BF7\u52A0\u7FA4\u65F6\uFF0C\u81EA\u52A8\u68C0\u6D4B\u5E76\u62E6\u622A\u9ED1\u540D\u5355\u7528\u6237\u3002\n- **\u624B\u52A8\u626B\u63CF**: \u63D0\u4F9B\u6307\u4EE4\u624B\u52A8\u626B\u63CF\u5F53\u524D\u6216\u5168\u90E8\u7FA4\u7EC4\u3002\n- **\u6743\u9650\u63A7\u5236**: \u4E25\u683C\u7684\u6307\u4EE4\u6743\u9650\u5206\u7EA7\u7BA1\u7406\u3002\n";
|
|
6
6
|
export declare const Config: Schema<PluginConfig>;
|
|
7
7
|
export declare function apply(ctx: Context, config: PluginConfig): void;
|
package/lib/index.js
CHANGED
|
@@ -3,19 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Config = exports.usage = exports.inject = exports.name = void 0;
|
|
4
4
|
exports.apply = apply;
|
|
5
5
|
const koishi_1 = require("koishi");
|
|
6
|
-
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
7
|
const core_1 = require("./core");
|
|
8
8
|
exports.name = 'blacklist-online';
|
|
9
9
|
exports.inject = ['database', 'http'];
|
|
10
10
|
exports.usage = `
|
|
11
11
|
## 功能说明
|
|
12
12
|
一个强大的、基于数据库的群组黑名单管理插件。
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
|
|
14
|
+
- **云端同步 & 自动巡航**: 实时同步云端黑名单。**当检测到新增黑名单用户时,会自动触发全局扫描**,及时清理所有群内的潜在威胁。
|
|
15
|
+
- **公开处刑**: 无论是自动扫描还是手动扫描,发现黑名单用户时均会在群内发送包含原因的通告(取决于群模式设置)。
|
|
16
|
+
- **受保护名单**: 配置中的用户(如群主、特定管理员)拥有豁免权,无法被拉黑。
|
|
17
|
+
- **分群管理**: 可为每个群独立设置处理模式(仅通知 / 仅踢出 / 通知并踢出 / 关闭)。
|
|
18
|
+
- **入群检测**: 新成员进群或申请加群时,自动检测并拦截黑名单用户。
|
|
17
19
|
- **手动扫描**: 提供指令手动扫描当前或全部群组。
|
|
18
|
-
- **权限控制**:
|
|
20
|
+
- **权限控制**: 严格的指令权限分级管理。
|
|
19
21
|
`;
|
|
20
22
|
// --- Schema 定义 ---
|
|
21
23
|
exports.Config = koishi_1.Schema.intersect([
|
|
@@ -79,13 +81,23 @@ function apply(ctx, config) {
|
|
|
79
81
|
logger.info(`📱 当前实例 UUID: ${entries[0].value}`);
|
|
80
82
|
}
|
|
81
83
|
// 启动时立即同步一次
|
|
82
|
-
(0, core_1.syncBlacklist)(ctx, config);
|
|
84
|
+
const hasUpdates = await (0, core_1.syncBlacklist)(ctx, config);
|
|
83
85
|
// 启动时处理积压队列
|
|
84
86
|
(0, core_1.processOfflineQueue)(ctx, config);
|
|
87
|
+
// 如果启动同步有更新,触发全群扫描
|
|
88
|
+
if (hasUpdates) {
|
|
89
|
+
(0, core_1.scanAllGuilds)(ctx, config);
|
|
90
|
+
}
|
|
85
91
|
});
|
|
86
92
|
// 3. 定时任务
|
|
87
|
-
ctx.setInterval(() =>
|
|
88
|
-
|
|
93
|
+
ctx.setInterval(async () => {
|
|
94
|
+
// 每次定时同步,如果有更新,就触发扫描
|
|
95
|
+
const hasUpdates = await (0, core_1.syncBlacklist)(ctx, config);
|
|
96
|
+
if (hasUpdates) {
|
|
97
|
+
await (0, core_1.scanAllGuilds)(ctx, config);
|
|
98
|
+
}
|
|
99
|
+
}, 5 * koishi_1.Time.minute); // 每5分同步
|
|
100
|
+
ctx.setInterval(() => (0, core_1.processOfflineQueue)(ctx, config), koishi_1.Time.minute); // 每分钟同步
|
|
89
101
|
// 4. 事件监听
|
|
90
102
|
// 监听加群申请 (自动拒绝)
|
|
91
103
|
ctx.on('guild-member-request', async (session) => {
|
|
@@ -131,12 +143,12 @@ function apply(ctx, config) {
|
|
|
131
143
|
return '❌ 该用户在本地白名单中,无法拉黑。';
|
|
132
144
|
const requestId = (0, node_crypto_1.randomUUID)();
|
|
133
145
|
const payload = {
|
|
134
|
-
|
|
146
|
+
request_id: requestId,
|
|
135
147
|
type: 'ADD',
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
applicant_id: session.userId,
|
|
149
|
+
target_user_id: userId,
|
|
138
150
|
reason,
|
|
139
|
-
|
|
151
|
+
guild_id: session.guildId,
|
|
140
152
|
timestamp: Date.now()
|
|
141
153
|
};
|
|
142
154
|
try {
|
|
@@ -161,10 +173,10 @@ function apply(ctx, config) {
|
|
|
161
173
|
const userId = (0, core_1.parseUserId)(user);
|
|
162
174
|
const requestId = (0, node_crypto_1.randomUUID)();
|
|
163
175
|
const payload = {
|
|
164
|
-
|
|
176
|
+
request_id: requestId,
|
|
165
177
|
type: 'REMOVE',
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
applicant_id: session.userId,
|
|
179
|
+
target_user_id: userId,
|
|
168
180
|
reason,
|
|
169
181
|
timestamp: Date.now()
|
|
170
182
|
};
|
|
@@ -185,9 +197,9 @@ function apply(ctx, config) {
|
|
|
185
197
|
if (!uuid)
|
|
186
198
|
return '请输入要撤回的申请 UUID。';
|
|
187
199
|
const payload = {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
request_id: (0, node_crypto_1.randomUUID)(),
|
|
201
|
+
target_request_id: uuid,
|
|
202
|
+
applicant_id: session?.userId,
|
|
191
203
|
timestamp: Date.now()
|
|
192
204
|
};
|
|
193
205
|
try {
|