koishi-plugin-new-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/lib/index.d.ts +151 -0
- package/lib/index.js +637 -0
- package/newauth.md +1458 -0
- package/package.json +44 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NewAuthService = exports.Config = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
exports.name = 'new-auth';
|
|
7
|
+
exports.inject = ['database'];
|
|
8
|
+
const BUILTIN_ROLES = [
|
|
9
|
+
{
|
|
10
|
+
id: 'bot-admin',
|
|
11
|
+
name: 'Bot 管理员',
|
|
12
|
+
type: 'builtin',
|
|
13
|
+
scopeType: 'global',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'guild-owner',
|
|
17
|
+
name: '群主',
|
|
18
|
+
type: 'builtin',
|
|
19
|
+
scopeType: 'guild',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'guild-admin',
|
|
23
|
+
name: '群管理员',
|
|
24
|
+
type: 'builtin',
|
|
25
|
+
scopeType: 'guild',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'guild-member',
|
|
29
|
+
name: '群成员',
|
|
30
|
+
type: 'builtin',
|
|
31
|
+
scopeType: 'guild',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'guest',
|
|
35
|
+
name: '访客',
|
|
36
|
+
type: 'builtin',
|
|
37
|
+
scopeType: 'global',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
exports.Config = koishi_1.Schema.object({
|
|
41
|
+
botAdmins: koishi_1.Schema.array(String)
|
|
42
|
+
.role('table')
|
|
43
|
+
.description('显式 Bot 管理员 UID,格式为 platform:userId。')
|
|
44
|
+
.default([]),
|
|
45
|
+
trustLegacyAuthorityAsAdmin: koishi_1.Schema.boolean()
|
|
46
|
+
.description('兼容旧实例:是否把 authority 达到阈值的用户视为 Bot 管理员。')
|
|
47
|
+
.default(true),
|
|
48
|
+
legacyAdminAuthority: koishi_1.Schema.number()
|
|
49
|
+
.min(0)
|
|
50
|
+
.max(10)
|
|
51
|
+
.description('旧 authority 管理员阈值。')
|
|
52
|
+
.default(4),
|
|
53
|
+
ownerRoleNames: koishi_1.Schema.array(String)
|
|
54
|
+
.description('平台角色名/ID 命中这些值时识别为群主。')
|
|
55
|
+
.default(['owner', 'guild-owner', 'administrator']),
|
|
56
|
+
adminRoleNames: koishi_1.Schema.array(String)
|
|
57
|
+
.description('平台角色名/ID 命中这些值时识别为群管理员。')
|
|
58
|
+
.default(['admin', 'administrator', 'guild-admin', 'moderator']),
|
|
59
|
+
allowGuildOverrideAuthorityMax: koishi_1.Schema.number()
|
|
60
|
+
.min(0)
|
|
61
|
+
.max(10)
|
|
62
|
+
.description('旧 authority 建议值不高于该值时,默认认为允许群内自治。')
|
|
63
|
+
.default(3),
|
|
64
|
+
deniedMessage: koishi_1.Schema.string()
|
|
65
|
+
.description('权限不足时返回的提示。')
|
|
66
|
+
.default('你没有权限使用该指令。'),
|
|
67
|
+
grantRuntimeCommandPermission: koishi_1.Schema.boolean()
|
|
68
|
+
.description('新权限系统放行后,临时授予本次 Koishi command 权限,绕过旧 command authority 校验。')
|
|
69
|
+
.default(true),
|
|
70
|
+
raiseLegacyAuthority: koishi_1.Schema.boolean()
|
|
71
|
+
.description('新权限系统放行后,临时提升 session.user.authority 以兼容插件内部硬编码判断。可能影响依赖旧 authority 的插件行为。')
|
|
72
|
+
.default(false),
|
|
73
|
+
});
|
|
74
|
+
function apply(ctx, config) {
|
|
75
|
+
ctx.model.extend('new_auth_command', {
|
|
76
|
+
id: 'string(255)',
|
|
77
|
+
name: 'string(255)',
|
|
78
|
+
commandPath: 'string(255)',
|
|
79
|
+
aliases: 'list',
|
|
80
|
+
plugin: 'string(255)',
|
|
81
|
+
description: 'text',
|
|
82
|
+
legacyAuthority: 'unsigned(4)',
|
|
83
|
+
status: 'string(32)',
|
|
84
|
+
allowGuildOverride: 'boolean',
|
|
85
|
+
createdAt: 'timestamp',
|
|
86
|
+
updatedAt: 'timestamp',
|
|
87
|
+
}, {
|
|
88
|
+
primary: 'id',
|
|
89
|
+
});
|
|
90
|
+
ctx.model.extend('new_auth_role', {
|
|
91
|
+
id: 'string(255)',
|
|
92
|
+
name: 'string(255)',
|
|
93
|
+
type: 'string(32)',
|
|
94
|
+
scopeType: 'string(32)',
|
|
95
|
+
builtin: 'boolean',
|
|
96
|
+
createdAt: 'timestamp',
|
|
97
|
+
updatedAt: 'timestamp',
|
|
98
|
+
}, {
|
|
99
|
+
primary: 'id',
|
|
100
|
+
});
|
|
101
|
+
ctx.model.extend('new_auth_role_member', {
|
|
102
|
+
roleId: 'string(255)',
|
|
103
|
+
platform: 'string(255)',
|
|
104
|
+
userId: 'string(255)',
|
|
105
|
+
scope: 'string(255)',
|
|
106
|
+
createdAt: 'timestamp',
|
|
107
|
+
}, {
|
|
108
|
+
primary: ['roleId', 'platform', 'userId', 'scope'],
|
|
109
|
+
});
|
|
110
|
+
ctx.model.extend('new_auth_policy', {
|
|
111
|
+
scope: 'string(255)',
|
|
112
|
+
roleId: 'string(255)',
|
|
113
|
+
commandId: 'string(255)',
|
|
114
|
+
state: 'string(32)',
|
|
115
|
+
updatedAt: 'timestamp',
|
|
116
|
+
}, {
|
|
117
|
+
primary: ['scope', 'roleId', 'commandId'],
|
|
118
|
+
});
|
|
119
|
+
const service = new NewAuthService(ctx, config);
|
|
120
|
+
ctx.provide('newauth', service);
|
|
121
|
+
ctx.on('ready', () => service.start());
|
|
122
|
+
ctx.on('command-added', command => service.registerCommand(command));
|
|
123
|
+
ctx.on('command-updated', command => service.registerCommand(command));
|
|
124
|
+
ctx.before('command/execute', async (argv) => {
|
|
125
|
+
return service.intercept(argv);
|
|
126
|
+
});
|
|
127
|
+
registerManagementCommands(ctx, config, service);
|
|
128
|
+
}
|
|
129
|
+
class NewAuthService {
|
|
130
|
+
constructor(ctx, config) {
|
|
131
|
+
this.ctx = ctx;
|
|
132
|
+
this.config = config;
|
|
133
|
+
this.commandCache = new Map();
|
|
134
|
+
this.adminSet = new Set(config.botAdmins.map(normalizeKey));
|
|
135
|
+
this.ownerRoleNames = new Set(config.ownerRoleNames.map(normalizeKey));
|
|
136
|
+
this.adminRoleNames = new Set(config.adminRoleNames.map(normalizeKey));
|
|
137
|
+
}
|
|
138
|
+
async start() {
|
|
139
|
+
await this.ensureBuiltinRoles();
|
|
140
|
+
for (const command of this.getCommandList()) {
|
|
141
|
+
await this.registerCommand(command);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async registerCommand(command) {
|
|
145
|
+
if (!command?.name)
|
|
146
|
+
return;
|
|
147
|
+
const now = new Date();
|
|
148
|
+
const record = this.createCommandRecord(command, now);
|
|
149
|
+
const [existing] = await this.ctx.database.get('new_auth_command', { id: record.id });
|
|
150
|
+
if (existing) {
|
|
151
|
+
const next = {
|
|
152
|
+
name: record.name,
|
|
153
|
+
commandPath: record.commandPath,
|
|
154
|
+
aliases: record.aliases,
|
|
155
|
+
plugin: record.plugin,
|
|
156
|
+
description: record.description,
|
|
157
|
+
legacyAuthority: record.legacyAuthority,
|
|
158
|
+
updatedAt: now,
|
|
159
|
+
};
|
|
160
|
+
await this.ctx.database.set('new_auth_command', record.id, next);
|
|
161
|
+
this.commandCache.set(record.id, { ...existing, ...next });
|
|
162
|
+
return record.id;
|
|
163
|
+
}
|
|
164
|
+
await this.ctx.database.create('new_auth_command', record);
|
|
165
|
+
this.commandCache.set(record.id, record);
|
|
166
|
+
if (this.isSelfCommand(command)) {
|
|
167
|
+
await this.setPolicy('global', 'bot-admin', record.id, 'allow');
|
|
168
|
+
}
|
|
169
|
+
return record.id;
|
|
170
|
+
}
|
|
171
|
+
async intercept(argv) {
|
|
172
|
+
const { command, session, options } = argv;
|
|
173
|
+
if (!command || !session)
|
|
174
|
+
return;
|
|
175
|
+
const commandId = await this.registerCommand(command);
|
|
176
|
+
if (!commandId)
|
|
177
|
+
return;
|
|
178
|
+
const result = await this.canExecute(session, commandId);
|
|
179
|
+
if (!result.allowed) {
|
|
180
|
+
return command.config.showWarning ? this.config.deniedMessage : '';
|
|
181
|
+
}
|
|
182
|
+
if (this.config.grantRuntimeCommandPermission) {
|
|
183
|
+
this.grantRuntimeCommandPermission(session, command, options || {});
|
|
184
|
+
}
|
|
185
|
+
if (this.config.raiseLegacyAuthority && session.user) {
|
|
186
|
+
const user = session.user;
|
|
187
|
+
const legacyAuthority = result.command?.legacyAuthority ?? command.config.authority ?? 0;
|
|
188
|
+
if ((user.authority ?? 0) < legacyAuthority) {
|
|
189
|
+
user.authority = legacyAuthority;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async canExecute(session, commandId) {
|
|
194
|
+
const command = await this.getCommand(commandId);
|
|
195
|
+
if (!command) {
|
|
196
|
+
return { allowed: await this.isBotAdmin(session), reason: 'command_missing' };
|
|
197
|
+
}
|
|
198
|
+
if (command.status === 'disabled') {
|
|
199
|
+
return { allowed: false, reason: 'command_disabled', command };
|
|
200
|
+
}
|
|
201
|
+
const roles = await this.resolveRoles(session);
|
|
202
|
+
if (roles.includes('bot-admin')) {
|
|
203
|
+
return { allowed: true, reason: 'bot_admin', command, roles };
|
|
204
|
+
}
|
|
205
|
+
if (command.status === 'pending') {
|
|
206
|
+
return { allowed: false, reason: 'command_pending', command, roles };
|
|
207
|
+
}
|
|
208
|
+
const scope = getScope(session);
|
|
209
|
+
for (const roleId of roles) {
|
|
210
|
+
const state = await this.getEffectivePolicy(scope, roleId, commandId);
|
|
211
|
+
if (state === 'allow') {
|
|
212
|
+
return { allowed: true, reason: 'role_policy', command, roles, matchedRole: roleId, matchedScope: scope };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { allowed: false, reason: 'no_policy', command, roles };
|
|
216
|
+
}
|
|
217
|
+
async resolveRoles(session) {
|
|
218
|
+
const roles = new Set();
|
|
219
|
+
const scope = getScope(session);
|
|
220
|
+
if (await this.isBotAdmin(session)) {
|
|
221
|
+
roles.add('bot-admin');
|
|
222
|
+
}
|
|
223
|
+
if (!session.userId) {
|
|
224
|
+
roles.add('guest');
|
|
225
|
+
}
|
|
226
|
+
else if (!session.guildId || session.isDirect) {
|
|
227
|
+
roles.add('guest');
|
|
228
|
+
}
|
|
229
|
+
else if (this.hasPlatformRole(session, this.ownerRoleNames)) {
|
|
230
|
+
roles.add('guild-owner');
|
|
231
|
+
}
|
|
232
|
+
else if (this.hasPlatformRole(session, this.adminRoleNames)) {
|
|
233
|
+
roles.add('guild-admin');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
roles.add('guild-member');
|
|
237
|
+
}
|
|
238
|
+
if (session.platform && session.userId) {
|
|
239
|
+
const globalMembers = await this.ctx.database.get('new_auth_role_member', {
|
|
240
|
+
platform: session.platform,
|
|
241
|
+
userId: session.userId,
|
|
242
|
+
scope: 'global',
|
|
243
|
+
});
|
|
244
|
+
const scopedMembers = scope === 'global' ? [] : await this.ctx.database.get('new_auth_role_member', {
|
|
245
|
+
platform: session.platform,
|
|
246
|
+
userId: session.userId,
|
|
247
|
+
scope,
|
|
248
|
+
});
|
|
249
|
+
for (const member of [...globalMembers, ...scopedMembers]) {
|
|
250
|
+
roles.add(member.roleId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return [...roles];
|
|
254
|
+
}
|
|
255
|
+
async isBotAdmin(session) {
|
|
256
|
+
if (!session.platform || !session.userId)
|
|
257
|
+
return false;
|
|
258
|
+
const uid = getUid(session.platform, session.userId);
|
|
259
|
+
if (this.adminSet.has(uid))
|
|
260
|
+
return true;
|
|
261
|
+
const [member] = await this.ctx.database.get('new_auth_role_member', {
|
|
262
|
+
roleId: 'bot-admin',
|
|
263
|
+
platform: session.platform,
|
|
264
|
+
userId: session.userId,
|
|
265
|
+
scope: 'global',
|
|
266
|
+
});
|
|
267
|
+
if (member)
|
|
268
|
+
return true;
|
|
269
|
+
const user = session.user;
|
|
270
|
+
return !!(this.config.trustLegacyAuthorityAsAdmin
|
|
271
|
+
&& user
|
|
272
|
+
&& user.authority >= this.config.legacyAdminAuthority);
|
|
273
|
+
}
|
|
274
|
+
async listCommands(options = {}) {
|
|
275
|
+
const records = await this.ctx.database.get('new_auth_command', {});
|
|
276
|
+
return records
|
|
277
|
+
.filter(record => options.all || !options.pending || record.status === 'pending')
|
|
278
|
+
.filter(record => {
|
|
279
|
+
if (!options.query)
|
|
280
|
+
return true;
|
|
281
|
+
const query = normalizeKey(options.query);
|
|
282
|
+
return normalizeKey(record.id).includes(query)
|
|
283
|
+
|| normalizeKey(record.commandPath).includes(query)
|
|
284
|
+
|| normalizeKey(record.name).includes(query)
|
|
285
|
+
|| normalizeKey(record.plugin).includes(query);
|
|
286
|
+
})
|
|
287
|
+
.sort((a, b) => a.commandPath.localeCompare(b.commandPath));
|
|
288
|
+
}
|
|
289
|
+
async listRoles() {
|
|
290
|
+
const records = await this.ctx.database.get('new_auth_role', {});
|
|
291
|
+
return records.sort((a, b) => Number(a.builtin) - Number(b.builtin) || a.id.localeCompare(b.id));
|
|
292
|
+
}
|
|
293
|
+
async addBotAdmin(uid) {
|
|
294
|
+
const parsed = parseUid(uid);
|
|
295
|
+
await this.ensureRoleMember('bot-admin', parsed.platform, parsed.userId, 'global');
|
|
296
|
+
}
|
|
297
|
+
async removeBotAdmin(uid) {
|
|
298
|
+
const parsed = parseUid(uid);
|
|
299
|
+
await this.ctx.database.remove('new_auth_role_member', {
|
|
300
|
+
roleId: 'bot-admin',
|
|
301
|
+
platform: parsed.platform,
|
|
302
|
+
userId: parsed.userId,
|
|
303
|
+
scope: 'global',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
async createCustomRole(id, name, scopeType = 'guild') {
|
|
307
|
+
if (BUILTIN_ROLES.some(role => role.id === id)) {
|
|
308
|
+
throw new Error(`cannot overwrite builtin role: ${id}`);
|
|
309
|
+
}
|
|
310
|
+
const now = new Date();
|
|
311
|
+
const [existing] = await this.ctx.database.get('new_auth_role', { id });
|
|
312
|
+
const record = {
|
|
313
|
+
id,
|
|
314
|
+
name,
|
|
315
|
+
type: 'custom',
|
|
316
|
+
scopeType,
|
|
317
|
+
builtin: false,
|
|
318
|
+
createdAt: existing?.createdAt ?? now,
|
|
319
|
+
updatedAt: now,
|
|
320
|
+
};
|
|
321
|
+
if (existing) {
|
|
322
|
+
await this.ctx.database.set('new_auth_role', id, record);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
await this.ctx.database.create('new_auth_role', record);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async addRoleMember(roleId, uid, scope = 'global') {
|
|
329
|
+
const parsed = parseUid(uid);
|
|
330
|
+
await this.ensureRoleMember(roleId, parsed.platform, parsed.userId, scope);
|
|
331
|
+
}
|
|
332
|
+
async removeRoleMember(roleId, uid, scope = 'global') {
|
|
333
|
+
const parsed = parseUid(uid);
|
|
334
|
+
await this.ctx.database.remove('new_auth_role_member', {
|
|
335
|
+
roleId,
|
|
336
|
+
platform: parsed.platform,
|
|
337
|
+
userId: parsed.userId,
|
|
338
|
+
scope,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async setCommandStatus(input, status) {
|
|
342
|
+
const command = await this.resolveCommandInput(input);
|
|
343
|
+
await this.ctx.database.set('new_auth_command', command.id, {
|
|
344
|
+
status,
|
|
345
|
+
updatedAt: new Date(),
|
|
346
|
+
});
|
|
347
|
+
this.commandCache.delete(command.id);
|
|
348
|
+
return command;
|
|
349
|
+
}
|
|
350
|
+
async setCommandPolicy(scope, roleId, input, state) {
|
|
351
|
+
const command = await this.resolveCommandInput(input);
|
|
352
|
+
await this.setPolicy(scope, roleId, command.id, state);
|
|
353
|
+
if (state !== 'inherit' && command.status === 'pending') {
|
|
354
|
+
await this.setCommandStatus(command.id, 'configured');
|
|
355
|
+
}
|
|
356
|
+
return command;
|
|
357
|
+
}
|
|
358
|
+
async getCommand(input) {
|
|
359
|
+
const cached = this.commandCache.get(input);
|
|
360
|
+
if (cached)
|
|
361
|
+
return cached;
|
|
362
|
+
const [command] = await this.ctx.database.get('new_auth_command', { id: input });
|
|
363
|
+
if (command)
|
|
364
|
+
this.commandCache.set(input, command);
|
|
365
|
+
return command;
|
|
366
|
+
}
|
|
367
|
+
async ensureBuiltinRoles() {
|
|
368
|
+
for (const role of BUILTIN_ROLES) {
|
|
369
|
+
const now = new Date();
|
|
370
|
+
const [existing] = await this.ctx.database.get('new_auth_role', { id: role.id });
|
|
371
|
+
const record = {
|
|
372
|
+
...role,
|
|
373
|
+
builtin: true,
|
|
374
|
+
createdAt: existing?.createdAt ?? now,
|
|
375
|
+
updatedAt: now,
|
|
376
|
+
};
|
|
377
|
+
if (existing) {
|
|
378
|
+
await this.ctx.database.set('new_auth_role', role.id, record);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
await this.ctx.database.create('new_auth_role', record);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
createCommandRecord(command, now) {
|
|
386
|
+
const plugin = inferPlugin(command);
|
|
387
|
+
const legacyAuthority = inferLegacyAuthority(command);
|
|
388
|
+
const commandPath = command.name;
|
|
389
|
+
const aliases = Object.keys(command._aliases || {});
|
|
390
|
+
const id = makeCommandId(plugin, commandPath);
|
|
391
|
+
return {
|
|
392
|
+
id,
|
|
393
|
+
name: command.displayName || commandPath,
|
|
394
|
+
commandPath,
|
|
395
|
+
aliases,
|
|
396
|
+
plugin,
|
|
397
|
+
description: this.getDescription(command),
|
|
398
|
+
legacyAuthority,
|
|
399
|
+
status: this.isSelfCommand(command) ? 'configured' : 'pending',
|
|
400
|
+
allowGuildOverride: legacyAuthority <= this.config.allowGuildOverrideAuthorityMax,
|
|
401
|
+
createdAt: now,
|
|
402
|
+
updatedAt: now,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
getDescription(command) {
|
|
406
|
+
const key = `commands.${command.name}.description`;
|
|
407
|
+
const value = this.ctx.i18n.get(key);
|
|
408
|
+
return Array.isArray(value) ? value[0] ?? '' : value ?? '';
|
|
409
|
+
}
|
|
410
|
+
async resolveCommandInput(input) {
|
|
411
|
+
const normalized = input.trim();
|
|
412
|
+
const [byId] = await this.ctx.database.get('new_auth_command', { id: normalized });
|
|
413
|
+
if (byId)
|
|
414
|
+
return byId;
|
|
415
|
+
const records = await this.ctx.database.get('new_auth_command', {});
|
|
416
|
+
const match = records.find(record => record.commandPath === normalized
|
|
417
|
+
|| record.name === normalized
|
|
418
|
+
|| record.aliases.includes(normalized));
|
|
419
|
+
if (!match)
|
|
420
|
+
throw new Error(`command not found: ${input}`);
|
|
421
|
+
return match;
|
|
422
|
+
}
|
|
423
|
+
async setPolicy(scope, roleId, commandId, state) {
|
|
424
|
+
const now = new Date();
|
|
425
|
+
const query = { scope, roleId, commandId };
|
|
426
|
+
const [existing] = await this.ctx.database.get('new_auth_policy', query);
|
|
427
|
+
if (existing) {
|
|
428
|
+
await this.ctx.database.set('new_auth_policy', query, { state, updatedAt: now });
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
await this.ctx.database.create('new_auth_policy', { ...query, state, updatedAt: now });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async getEffectivePolicy(scope, roleId, commandId) {
|
|
435
|
+
if (scope !== 'global') {
|
|
436
|
+
const [scoped] = await this.ctx.database.get('new_auth_policy', { scope, roleId, commandId });
|
|
437
|
+
if (scoped && scoped.state !== 'inherit')
|
|
438
|
+
return scoped.state;
|
|
439
|
+
}
|
|
440
|
+
const [global] = await this.ctx.database.get('new_auth_policy', {
|
|
441
|
+
scope: 'global',
|
|
442
|
+
roleId,
|
|
443
|
+
commandId,
|
|
444
|
+
});
|
|
445
|
+
return global?.state ?? 'inherit';
|
|
446
|
+
}
|
|
447
|
+
async ensureRoleMember(roleId, platform, userId, scope) {
|
|
448
|
+
const [role] = await this.ctx.database.get('new_auth_role', { id: roleId });
|
|
449
|
+
if (!role)
|
|
450
|
+
throw new Error(`role not found: ${roleId}`);
|
|
451
|
+
const query = { roleId, platform, userId, scope };
|
|
452
|
+
const [existing] = await this.ctx.database.get('new_auth_role_member', query);
|
|
453
|
+
if (!existing) {
|
|
454
|
+
await this.ctx.database.create('new_auth_role_member', { ...query, createdAt: new Date() });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
hasPlatformRole(session, roleNames) {
|
|
458
|
+
const values = new Set();
|
|
459
|
+
const author = session.author;
|
|
460
|
+
const member = session.event?.member;
|
|
461
|
+
addRoleValue(values, author?.role);
|
|
462
|
+
addRoleValue(values, author?.roles);
|
|
463
|
+
addRoleValue(values, member?.role);
|
|
464
|
+
addRoleValue(values, member?.roles);
|
|
465
|
+
return [...values].some(value => roleNames.has(normalizeKey(value)));
|
|
466
|
+
}
|
|
467
|
+
grantRuntimeCommandPermission(session, command, options) {
|
|
468
|
+
session.permissions || (session.permissions = []);
|
|
469
|
+
const permissions = [`command:${command.name}`];
|
|
470
|
+
for (const option of Object.values(command._options || {})) {
|
|
471
|
+
if (option.name in options) {
|
|
472
|
+
permissions.push(`command:${command.name}:option:${option.name}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const permission of permissions) {
|
|
476
|
+
if (!session.permissions.includes(permission)) {
|
|
477
|
+
session.permissions.push(permission);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
getCommandList() {
|
|
482
|
+
return [...(this.ctx.$commander?._commandList || [])];
|
|
483
|
+
}
|
|
484
|
+
isSelfCommand(command) {
|
|
485
|
+
return command.name === 'newauth' || command.name.startsWith('newauth/');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
exports.NewAuthService = NewAuthService;
|
|
489
|
+
function registerManagementCommands(ctx, config, service) {
|
|
490
|
+
const authority = config.legacyAdminAuthority;
|
|
491
|
+
ctx.command('newauth', '管理新权限系统', { authority })
|
|
492
|
+
.action(async ({ session }) => {
|
|
493
|
+
const commands = await service.listCommands({ all: true });
|
|
494
|
+
const pending = commands.filter(command => command.status === 'pending').length;
|
|
495
|
+
const roles = await service.listRoles();
|
|
496
|
+
return [
|
|
497
|
+
`已登记指令:${commands.length}`,
|
|
498
|
+
`待配置指令:${pending}`,
|
|
499
|
+
`角色数量:${roles.length}`,
|
|
500
|
+
].join('\n');
|
|
501
|
+
});
|
|
502
|
+
ctx.command('newauth.commands [query:text]', '列出已登记指令', { authority })
|
|
503
|
+
.option('pending', '-p 只显示待配置指令')
|
|
504
|
+
.option('all', '-a 显示全部指令')
|
|
505
|
+
.action(async ({ options = {} }, query) => {
|
|
506
|
+
const commands = await service.listCommands({
|
|
507
|
+
pending: !!options.pending,
|
|
508
|
+
all: !!options.all,
|
|
509
|
+
query,
|
|
510
|
+
});
|
|
511
|
+
if (!commands.length)
|
|
512
|
+
return '没有匹配的指令。';
|
|
513
|
+
return commands.map(command => {
|
|
514
|
+
return `${command.status.padEnd(10)} ${command.commandPath} (${command.id}) legacy=${command.legacyAuthority}`;
|
|
515
|
+
}).join('\n');
|
|
516
|
+
});
|
|
517
|
+
ctx.command('newauth.roles', '列出角色', { authority })
|
|
518
|
+
.action(async () => {
|
|
519
|
+
const roles = await service.listRoles();
|
|
520
|
+
return roles.map(role => {
|
|
521
|
+
const type = role.builtin ? 'builtin' : 'custom';
|
|
522
|
+
return `${role.id} ${role.name} ${type}/${role.scopeType}`;
|
|
523
|
+
}).join('\n');
|
|
524
|
+
});
|
|
525
|
+
ctx.command('newauth.allow <roleId> <command> [scope]', '允许角色使用指令', { authority })
|
|
526
|
+
.action(async (_, roleId, command, scope = 'global') => {
|
|
527
|
+
const record = await service.setCommandPolicy(scope, roleId, command, 'allow');
|
|
528
|
+
return `已允许:${scope} ${roleId} -> ${record.commandPath}`;
|
|
529
|
+
});
|
|
530
|
+
ctx.command('newauth.deny <roleId> <command> [scope]', '关闭角色使用指令', { authority })
|
|
531
|
+
.action(async (_, roleId, command, scope = 'global') => {
|
|
532
|
+
const record = await service.setCommandPolicy(scope, roleId, command, 'deny');
|
|
533
|
+
return `已关闭:${scope} ${roleId} -> ${record.commandPath}`;
|
|
534
|
+
});
|
|
535
|
+
ctx.command('newauth.inherit <roleId> <command> [scope]', '恢复继承策略', { authority })
|
|
536
|
+
.action(async (_, roleId, command, scope = 'global') => {
|
|
537
|
+
const record = await service.setCommandPolicy(scope, roleId, command, 'inherit');
|
|
538
|
+
return `已恢复继承:${scope} ${roleId} -> ${record.commandPath}`;
|
|
539
|
+
});
|
|
540
|
+
ctx.command('newauth.disable <command>', '禁用指令', { authority })
|
|
541
|
+
.action(async (_, command) => {
|
|
542
|
+
const record = await service.setCommandStatus(command, 'disabled');
|
|
543
|
+
return `已禁用:${record.commandPath}`;
|
|
544
|
+
});
|
|
545
|
+
ctx.command('newauth.enable <command>', '启用指令', { authority })
|
|
546
|
+
.action(async (_, command) => {
|
|
547
|
+
const record = await service.setCommandStatus(command, 'configured');
|
|
548
|
+
return `已启用:${record.commandPath}`;
|
|
549
|
+
});
|
|
550
|
+
ctx.command('newauth.admin.add <uid>', '添加 Bot 管理员', { authority })
|
|
551
|
+
.action(async (_, uid) => {
|
|
552
|
+
await service.addBotAdmin(uid);
|
|
553
|
+
return `已添加 Bot 管理员:${uid}`;
|
|
554
|
+
});
|
|
555
|
+
ctx.command('newauth.admin.remove <uid>', '移除 Bot 管理员', { authority })
|
|
556
|
+
.action(async (_, uid) => {
|
|
557
|
+
await service.removeBotAdmin(uid);
|
|
558
|
+
return `已移除 Bot 管理员:${uid}`;
|
|
559
|
+
});
|
|
560
|
+
ctx.command('newauth.role.create <id> <displayName> [scopeType]', '创建自定义角色', { authority })
|
|
561
|
+
.action(async (_, id, displayName, scopeType = 'guild') => {
|
|
562
|
+
if (scopeType !== 'global' && scopeType !== 'guild') {
|
|
563
|
+
return 'scopeType 只能是 global 或 guild。';
|
|
564
|
+
}
|
|
565
|
+
await service.createCustomRole(id, displayName, scopeType);
|
|
566
|
+
return `已创建角色:${id}`;
|
|
567
|
+
});
|
|
568
|
+
ctx.command('newauth.member.add <roleId> <uid> [scope]', '添加角色成员', { authority })
|
|
569
|
+
.action(async (_, roleId, uid, scope = 'global') => {
|
|
570
|
+
await service.addRoleMember(roleId, uid, scope);
|
|
571
|
+
return `已添加成员:${roleId} ${uid} ${scope}`;
|
|
572
|
+
});
|
|
573
|
+
ctx.command('newauth.member.remove <roleId> <uid> [scope]', '移除角色成员', { authority })
|
|
574
|
+
.action(async (_, roleId, uid, scope = 'global') => {
|
|
575
|
+
await service.removeRoleMember(roleId, uid, scope);
|
|
576
|
+
return `已移除成员:${roleId} ${uid} ${scope}`;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function inferLegacyAuthority(command) {
|
|
580
|
+
if (typeof command.config.authority === 'number')
|
|
581
|
+
return command.config.authority;
|
|
582
|
+
for (const permission of command.config.permissions || []) {
|
|
583
|
+
const capture = /^authority:(\d+)$/.exec(permission);
|
|
584
|
+
if (capture)
|
|
585
|
+
return +capture[1];
|
|
586
|
+
}
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
589
|
+
function inferPlugin(command) {
|
|
590
|
+
const source = command.caller?.scope || command.ctx?.scope;
|
|
591
|
+
const plugin = source?.plugin?.name || source?.uid || source?.id || 'unknown';
|
|
592
|
+
return String(plugin).replace(/^koishi-plugin-/, '') || 'unknown';
|
|
593
|
+
}
|
|
594
|
+
function makeCommandId(plugin, commandPath) {
|
|
595
|
+
return `command:${normalizeSegment(plugin)}:${normalizeSegment(commandPath)}`;
|
|
596
|
+
}
|
|
597
|
+
function normalizeSegment(value) {
|
|
598
|
+
return value.trim().toLowerCase().replace(/\s+/g, '-').replace(/[/:]+/g, '.');
|
|
599
|
+
}
|
|
600
|
+
function getScope(session) {
|
|
601
|
+
if (session.platform && session.guildId && !session.isDirect) {
|
|
602
|
+
return `guild:${session.platform}:${session.guildId}`;
|
|
603
|
+
}
|
|
604
|
+
return 'global';
|
|
605
|
+
}
|
|
606
|
+
function parseUid(uid) {
|
|
607
|
+
const index = uid.indexOf(':');
|
|
608
|
+
if (index <= 0 || index === uid.length - 1) {
|
|
609
|
+
throw new Error('uid must be platform:userId');
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
platform: uid.slice(0, index),
|
|
613
|
+
userId: uid.slice(index + 1),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function getUid(platform, userId) {
|
|
617
|
+
return normalizeKey(`${platform}:${userId}`);
|
|
618
|
+
}
|
|
619
|
+
function normalizeKey(value) {
|
|
620
|
+
return String(value).trim().toLowerCase();
|
|
621
|
+
}
|
|
622
|
+
function addRoleValue(target, value) {
|
|
623
|
+
if (!value)
|
|
624
|
+
return;
|
|
625
|
+
if (Array.isArray(value)) {
|
|
626
|
+
for (const item of value)
|
|
627
|
+
addRoleValue(target, item);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (typeof value === 'object') {
|
|
631
|
+
const role = value;
|
|
632
|
+
addRoleValue(target, role.id);
|
|
633
|
+
addRoleValue(target, role.name);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
target.add(String(value));
|
|
637
|
+
}
|