koishi-plugin-aaqqbot 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/LICENSE +21 -0
- package/README.md +180 -0
- package/lib/aa.d.ts +91 -0
- package/lib/aa.js +255 -0
- package/lib/commands.d.ts +3 -0
- package/lib/commands.js +73 -0
- package/lib/config.d.ts +39 -0
- package/lib/config.js +91 -0
- package/lib/guard.d.ts +175 -0
- package/lib/guard.js +1361 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +32 -0
- package/lib/notifier.d.ts +24 -0
- package/lib/notifier.js +126 -0
- package/lib/platform.d.ts +34 -0
- package/lib/platform.js +116 -0
- package/lib/policy.d.ts +122 -0
- package/lib/policy.js +203 -0
- package/lib/signing.d.ts +16 -0
- package/lib/signing.js +39 -0
- package/lib/store.d.ts +81 -0
- package/lib/store.js +140 -0
- package/lib/texts.d.ts +4 -0
- package/lib/texts.js +90 -0
- package/lib/util.d.ts +34 -0
- package/lib/util.js +140 -0
- package/package.json +63 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.MODE_RANK = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
exports.MODE_RANK = { off: 0, report: 1, remind: 2, enforce: 3 };
|
|
6
|
+
const modeSchema = koishi_1.Schema.union([
|
|
7
|
+
koishi_1.Schema.const('off').description('off:不管这个群'),
|
|
8
|
+
koishi_1.Schema.const('report').description('report:只在运维群报告,群成员感觉不到'),
|
|
9
|
+
koishi_1.Schema.const('remind').description('remind:群里 @ 提醒 + 名片加标记,不踢'),
|
|
10
|
+
koishi_1.Schema.const('enforce').description('enforce:提醒 + 宽限期到了移出'),
|
|
11
|
+
]);
|
|
12
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
13
|
+
koishi_1.Schema.object({
|
|
14
|
+
aaBaseUrl: koishi_1.Schema.string().role('link').required()
|
|
15
|
+
.description('AA 的网址,例如 `https://auth.example.com`。AA 装在子路径下时连子路径一起写,例如 `https://example.com/auth`。正式环境必须用 https。'),
|
|
16
|
+
keyId: koishi_1.Schema.string().pattern(/^[\x21-\x7e]{1,64}$/).required()
|
|
17
|
+
.description('密钥编号:AA 的 `local.py` 里 `QQBOT_API_KEYS` 的键,例如 `koishi-1`。只能用英文、数字和符号,不能有空格。'),
|
|
18
|
+
secret: koishi_1.Schema.string().role('secret').required()
|
|
19
|
+
.description('密钥:`QQBOT_API_KEYS` 里对应的值。只填在这里,不要发给任何人。'),
|
|
20
|
+
timeoutSeconds: koishi_1.Schema.natural().min(5).max(120).default(30)
|
|
21
|
+
.description('每次请求 AA 最多等多少秒。'),
|
|
22
|
+
bindUrl: koishi_1.Schema.string().role('link').default('')
|
|
23
|
+
.description('成员去绑定 QQ 的网址,会写进拒绝理由和提醒里。留空时使用「AA 的网址 + /services/」。'),
|
|
24
|
+
}).description('连接 AA'),
|
|
25
|
+
koishi_1.Schema.object({
|
|
26
|
+
botId: koishi_1.Schema.string().default('')
|
|
27
|
+
.description('用哪个机器人账号管理(填机器人的 QQ 号)。这个 Koishi 里只有一个 QQ 机器人时可以留空。'),
|
|
28
|
+
adminGroupId: koishi_1.Schema.string().default('')
|
|
29
|
+
.description('运维群号:巡检汇总、报警都发到这里。只放管理人员,**不能是被管理的群**。管理命令也只能在这个群里或私聊机器人时使用。'),
|
|
30
|
+
operators: koishi_1.Schema.array(koishi_1.Schema.string()).role('table').default([])
|
|
31
|
+
.description('运维名单(QQ 号):只有名单里、并且 Koishi 权限等级 ≥ 3 的人能用管理命令。'),
|
|
32
|
+
whitelist: koishi_1.Schema.array(koishi_1.Schema.string()).role('table').default([])
|
|
33
|
+
.description('白名单(QQ 号):这些人永远不会被提醒、改名片或移出。机器人自己、群主、群管理员已自动保护,不用填。'),
|
|
34
|
+
}).description('机器人与运维'),
|
|
35
|
+
koishi_1.Schema.object({
|
|
36
|
+
defaultMode: modeSchema.default('report')
|
|
37
|
+
.description('AA 上新加的群默认用哪种模式。'),
|
|
38
|
+
groupModes: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
39
|
+
groupId: koishi_1.Schema.string().required().description('群号'),
|
|
40
|
+
mode: modeSchema.default('report').description('模式'),
|
|
41
|
+
})).role('table').default([])
|
|
42
|
+
.description('单独设置某些群的模式。**升级到 remind 或 enforce 后,要等一轮巡检报告出来,由管理员发送 `aaqq.confirm 群号` 确认后才真正生效**;降级立即生效。'),
|
|
43
|
+
}).description('群模式'),
|
|
44
|
+
koishi_1.Schema.object({
|
|
45
|
+
autoReject: koishi_1.Schema.boolean().default(true)
|
|
46
|
+
.description('AA 明确判定不合格时自动拒绝入群申请(只在 remind / enforce 模式的群里)。关掉则留给管理员处理。AA 连不上、需要人工处理时永远不会拒绝。'),
|
|
47
|
+
rejectTemplate: koishi_1.Schema.string().default('{hint}。绑定地址:{url}')
|
|
48
|
+
.description('拒绝理由。`{hint}` 是自动生成的原因说明,`{url}` 是绑定网址。'),
|
|
49
|
+
inviteHandling: koishi_1.Schema.union([
|
|
50
|
+
koishi_1.Schema.const('same').description('和普通申请一样问 AA'),
|
|
51
|
+
koishi_1.Schema.const('manual').description('一律留给管理员'),
|
|
52
|
+
]).default('same')
|
|
53
|
+
.description('群成员邀请别人入群、需要审核时(申请里没有验证信息)怎么处理。'),
|
|
54
|
+
catchUpRequests: koishi_1.Schema.boolean().default(true)
|
|
55
|
+
.description('机器人重新上线时,补处理掉线期间还挂着的入群申请。'),
|
|
56
|
+
}).description('入群申请'),
|
|
57
|
+
koishi_1.Schema.object({
|
|
58
|
+
patrolIntervalHours: koishi_1.Schema.natural().min(1).max(24).default(6)
|
|
59
|
+
.description('每隔几小时巡检一次所有群(插件启动后会先巡检一次)。'),
|
|
60
|
+
eventPollSeconds: koishi_1.Schema.natural().min(30).max(600).default(60)
|
|
61
|
+
.description('每隔几秒向 AA 拉取一次变化(解绑、退组等),发现后立即复查相关的人。'),
|
|
62
|
+
syncCards: koishi_1.Schema.boolean().default(true)
|
|
63
|
+
.description('按 AA 算好的名片同步合格成员的群名片(只在 remind / enforce 模式的群里;成员自己改掉的会在下次巡检时改回)。'),
|
|
64
|
+
}).description('巡检与群名片'),
|
|
65
|
+
koishi_1.Schema.object({
|
|
66
|
+
graceHours: koishi_1.Schema.natural().min(1).max(720).default(48)
|
|
67
|
+
.description('enforce 模式下的宽限期:从第一次收到带截止时间的 @ 提醒开始算,过了时间还不合格才移出。移出前 36 小时内一定成功提醒过这个人。'),
|
|
68
|
+
remindTime: koishi_1.Schema.string().pattern(/^\s*([01]?\d|2[0-3]):[0-5]\d\s*$/).default('19:30')
|
|
69
|
+
.description('每天几点在群里 @ 提醒不合格的人(机器人电脑的本地时间,格式 `19:30`)。'),
|
|
70
|
+
remindTemplate: koishi_1.Schema.string().role('textarea').default('以下成员还没有满足本群的要求,请尽快在联盟 AA 完成 QQ 绑定:{url}\n{list}')
|
|
71
|
+
.description('remind 模式的提醒文字。`{list}` 是被 @ 的人和原因,`{url}` 是绑定网址。'),
|
|
72
|
+
warnTemplate: koishi_1.Schema.string().role('textarea').default('以下成员还没有满足本群的要求,请在截止时间前在联盟 AA 完成 QQ 绑定,否则会被移出本群:{url}\n{list}')
|
|
73
|
+
.description('enforce 模式的提醒文字。`{list}` 里会带上每个人的截止时间。'),
|
|
74
|
+
markCards: koishi_1.Schema.boolean().default(true)
|
|
75
|
+
.description('给不合格的人的群名片前面加标记(remind / enforce 模式)。合格后自动改成 AA 给的名片。'),
|
|
76
|
+
markPrefix: koishi_1.Schema.string().default('【SPY】')
|
|
77
|
+
.description('名片标记,例如 `【SPY】`,会加在原名片前面。'),
|
|
78
|
+
kickAnnounce: koishi_1.Schema.boolean().default(true)
|
|
79
|
+
.description('enforce 模式移出成员后,在该群里也发一条公告。'),
|
|
80
|
+
kickAnnounceTemplate: koishi_1.Schema.string().role('textarea').default('以下成员因未满足本群的要求已被移出:{list}\n完成联盟 AA 绑定后可以重新申请入群:{url}')
|
|
81
|
+
.description('移出公告。`{list}` 是被移出的人的名片。'),
|
|
82
|
+
}).description('宽限、提醒与标记'),
|
|
83
|
+
koishi_1.Schema.object({
|
|
84
|
+
breakerCount: koishi_1.Schema.natural().min(1).max(100).default(5)
|
|
85
|
+
.description('熔断人数:一轮里某个群新增的不合格人数超过这个数(或超过下面的比例,取较小者,但至少为 1)时,这个群这一轮什么都不做,只报警,等管理员发送 `aaqq.confirm 群号` 确认。'),
|
|
86
|
+
breakerPercent: koishi_1.Schema.natural().min(1).max(100).default(10)
|
|
87
|
+
.description('熔断比例(占群人数的百分比)。'),
|
|
88
|
+
kickPerHour: koishi_1.Schema.natural().min(1).max(100).default(10)
|
|
89
|
+
.description('每个群每小时最多移出几个人,超过的留到下一轮。'),
|
|
90
|
+
}).description('防误踢(熔断)'),
|
|
91
|
+
]);
|
package/lib/guard.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Bot, Context, Logger } from 'koishi';
|
|
2
|
+
import { AaClient, ManagedGroup } from './aa';
|
|
3
|
+
import { Config, Mode } from './config';
|
|
4
|
+
import { Notifier } from './notifier';
|
|
5
|
+
import { Platform } from './platform';
|
|
6
|
+
import { Member, Plan, Role } from './policy';
|
|
7
|
+
import { GroupState, Store, TrackedMember } from './store';
|
|
8
|
+
export interface GuardOptions {
|
|
9
|
+
now?: () => number;
|
|
10
|
+
/** 自动运行定时任务(测试里关掉,手动调用各方法)。 */
|
|
11
|
+
timers?: boolean;
|
|
12
|
+
/** AA 请求失败时的重试等待。 */
|
|
13
|
+
retryDelays?: number[];
|
|
14
|
+
/** 两次改名片之间的间隔。 */
|
|
15
|
+
cardDelayMs?: number;
|
|
16
|
+
/** 两次移出之间的间隔(实际在这个值到两倍之间随机)。 */
|
|
17
|
+
kickDelayMs?: number;
|
|
18
|
+
/** 巡检时两个群之间的间隔。 */
|
|
19
|
+
groupDelayMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ModeInfo {
|
|
22
|
+
desired: Mode;
|
|
23
|
+
effective: Mode;
|
|
24
|
+
/** 设了 remind / enforce 但还没确认。 */
|
|
25
|
+
awaiting: boolean;
|
|
26
|
+
held: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface ApplyResult {
|
|
29
|
+
cardsOk: number;
|
|
30
|
+
cardsFailed: number;
|
|
31
|
+
cardsDeferred: number;
|
|
32
|
+
marked: number;
|
|
33
|
+
unmarked: number;
|
|
34
|
+
synced: number;
|
|
35
|
+
kicked: Array<{
|
|
36
|
+
qq: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}>;
|
|
39
|
+
kickFailed: number;
|
|
40
|
+
kickSkipped: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class Guard {
|
|
43
|
+
private ctx;
|
|
44
|
+
config: Config;
|
|
45
|
+
private options;
|
|
46
|
+
readonly logger: Logger;
|
|
47
|
+
readonly aa: AaClient;
|
|
48
|
+
readonly store: Store;
|
|
49
|
+
readonly platform: Platform;
|
|
50
|
+
readonly notifier: Notifier;
|
|
51
|
+
groups: ManagedGroup[];
|
|
52
|
+
groupsLoaded: boolean;
|
|
53
|
+
paused: boolean;
|
|
54
|
+
/** 运维群号出现在受管群列表里时为 true(这时不发通知,见 R18)。 */
|
|
55
|
+
adminGroupConflict: boolean;
|
|
56
|
+
rosters: Map<string, Map<string, Member>>;
|
|
57
|
+
lastRound: {
|
|
58
|
+
at: number;
|
|
59
|
+
ok: boolean;
|
|
60
|
+
text: string;
|
|
61
|
+
} | null;
|
|
62
|
+
patrolRunning: boolean;
|
|
63
|
+
nextPatrolAt: number | null;
|
|
64
|
+
/** start() 的执行结果(测试里用来等待启动完成)。 */
|
|
65
|
+
started: Promise<void> | null;
|
|
66
|
+
private life;
|
|
67
|
+
private round;
|
|
68
|
+
private patrolQueue;
|
|
69
|
+
private eventsBusy;
|
|
70
|
+
private remindBusy;
|
|
71
|
+
private aaDown;
|
|
72
|
+
private botProblem;
|
|
73
|
+
private handledFlags;
|
|
74
|
+
private approved;
|
|
75
|
+
private timers;
|
|
76
|
+
private lastPrune;
|
|
77
|
+
constructor(ctx: Context, config: Config, options?: GuardOptions);
|
|
78
|
+
now(): number;
|
|
79
|
+
get signal(): AbortSignal;
|
|
80
|
+
adminGroup(): string | null;
|
|
81
|
+
operators(): Set<string>;
|
|
82
|
+
protectedIds(): Set<string>;
|
|
83
|
+
/**
|
|
84
|
+
* 事件游标和受管群列表是「某一个 AA」的数据:换了 AA 网址(例如从测试 AA 换到正式 AA)后要重新开始,
|
|
85
|
+
* 否则会拿测试 AA 的游标去读正式 AA,漏掉一批变化。
|
|
86
|
+
*/
|
|
87
|
+
get cursorKey(): string;
|
|
88
|
+
get groupsKey(): string;
|
|
89
|
+
private aaIdentity;
|
|
90
|
+
bindUrl(): string;
|
|
91
|
+
group(groupId: string): ManagedGroup | undefined;
|
|
92
|
+
groupLabel(groupId: string): string;
|
|
93
|
+
desiredMode(groupId: string): Mode;
|
|
94
|
+
modeInfo(groupId: string, state: GroupState): ModeInfo;
|
|
95
|
+
private writeMode;
|
|
96
|
+
install(): void;
|
|
97
|
+
dispose(): void;
|
|
98
|
+
/** 包一层 try/catch:Koishi 里同步抛错会让整个进程退出(R20)。 */
|
|
99
|
+
safely(what: string, task: () => unknown): void;
|
|
100
|
+
start(): Promise<void>;
|
|
101
|
+
private schedule;
|
|
102
|
+
private patrolTick;
|
|
103
|
+
private eventsTick;
|
|
104
|
+
private groupsTick;
|
|
105
|
+
private scheduleReminder;
|
|
106
|
+
/** 尽快巡检(全部群或指定的群)。正在巡检时,结束后立刻再跑一轮。 */
|
|
107
|
+
requestPatrol(groupIds?: string[]): void;
|
|
108
|
+
private noteAaFailure;
|
|
109
|
+
private noteAaOk;
|
|
110
|
+
private pickBot;
|
|
111
|
+
checkHealth(notify: boolean): Promise<string>;
|
|
112
|
+
refreshGroups(): Promise<boolean>;
|
|
113
|
+
/**
|
|
114
|
+
* 巡检一轮。返回 'busy'(上一轮还没结束)、'paused'、'no-bot'、'no-groups' 或 'done'。
|
|
115
|
+
* 同一时间只有一轮(R11);暂停、停用、改配置会立即中止(R10)。
|
|
116
|
+
*/
|
|
117
|
+
runPatrol(only?: string[]): Promise<'busy' | 'paused' | 'no-bot' | 'no-groups' | 'done'>;
|
|
118
|
+
private notifyBotProblemOnce;
|
|
119
|
+
patrolGroup(bot: Bot, g: ManagedGroup, signal: AbortSignal): Promise<{
|
|
120
|
+
ok: boolean;
|
|
121
|
+
text: string;
|
|
122
|
+
}>;
|
|
123
|
+
/** 管理员确认后 1 小时内有效的豁免。 */
|
|
124
|
+
private bypassFor;
|
|
125
|
+
/** 群改成 off 时,撤掉以前加的标记、清空宽限记录,之后就不再管这个群。 */
|
|
126
|
+
private cleanupOffGroup;
|
|
127
|
+
private planSettings;
|
|
128
|
+
private describePlan;
|
|
129
|
+
/**
|
|
130
|
+
* 执行规划。先移出、再改名片。每一次改动前都重新检查:没有中止、没有暂停、没有熔断;
|
|
131
|
+
* 移出前再问一次 AA、重读宽限记录、实时查询成员身份(R7)。通知失败不影响执行结果(R18)。
|
|
132
|
+
*/
|
|
133
|
+
applyPlan(bot: Bot, groupId: string, plan: Plan, signal: AbortSignal): Promise<ApplyResult>;
|
|
134
|
+
/** 还能不能继续改动这个群:没有中止、没有暂停、没有进入熔断。 */
|
|
135
|
+
private stillWritable;
|
|
136
|
+
private applyKicks;
|
|
137
|
+
private pause;
|
|
138
|
+
private onRequestSession;
|
|
139
|
+
handleJoinRequest(bot: Bot, req: {
|
|
140
|
+
flag: string;
|
|
141
|
+
groupId: string;
|
|
142
|
+
qq: string;
|
|
143
|
+
comment: string;
|
|
144
|
+
invitorId: string | null;
|
|
145
|
+
}, source?: string, catchUp?: boolean): Promise<void>;
|
|
146
|
+
/** 机器人重新上线:补处理掉线期间积压的入群申请(DECISIONS 第 13 条)。 */
|
|
147
|
+
private onBotStatus;
|
|
148
|
+
catchUpRequests(bot: Bot): Promise<void>;
|
|
149
|
+
private onMemberAdded;
|
|
150
|
+
handleNewMember(bot: Bot, groupId: string, qq: string): Promise<void>;
|
|
151
|
+
private onMemberRemoved;
|
|
152
|
+
pollEvents(): Promise<void>;
|
|
153
|
+
/** 游标丢失(第一次运行):把已有事件拉完只记游标,然后做一次完整巡检。 */
|
|
154
|
+
private initCursor;
|
|
155
|
+
/** 按群合并复查一批 QQ(每个群只发一次 check)。返回是否全部处理完。 */
|
|
156
|
+
recheck(qqs: string[]): Promise<boolean>;
|
|
157
|
+
runReminders(): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* 在群里 @ 提醒(每条最多 20 人)。enforce 模式下,第一次成功提醒某人时才定下他的截止时间
|
|
160
|
+
* (现在 + 宽限期),保证每个人被移出前都收到过带截止时间的提醒。
|
|
161
|
+
*/
|
|
162
|
+
sendReminder(bot: Bot, groupId: string, mode: Mode, rows: TrackedMember[]): Promise<void>;
|
|
163
|
+
setPaused(paused: boolean, actor: string): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* 管理员确认:模式升级生效和/或解除熔断,并马上重新巡检这个群。
|
|
166
|
+
* 接下来 1 小时内的那一轮巡检,只要人数不超过这次确认时报告里的人数,就不会再熔断。
|
|
167
|
+
*/
|
|
168
|
+
confirm(groupId: string, actor: string): Promise<string>;
|
|
169
|
+
statusText(): Promise<string>;
|
|
170
|
+
/** 查询一个 QQ 在各受管群的判定(只给运维用,输出不含角色名)。 */
|
|
171
|
+
checkQq(qq: string): Promise<string>;
|
|
172
|
+
private pruneMaps;
|
|
173
|
+
private pruneAudit;
|
|
174
|
+
}
|
|
175
|
+
export type { Role };
|