@helyx/bot 1.0.1 → 1.2.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/CHANGELOG.md +80 -0
- package/README.md +27 -2
- package/dist/auto-moderation-scanner-registry.d.ts +27 -0
- package/dist/auto-moderation-scanner-registry.js +172 -0
- package/dist/cloud-connector.js +3 -0
- package/dist/cloud-control-dispatcher.js +32 -0
- package/dist/command-path-contract.js +1 -0
- package/dist/control-permissions.d.ts +2 -2
- package/dist/control-permissions.js +4 -2
- package/dist/control-server.d.ts +72 -16
- package/dist/control-server.js +150 -250
- package/dist/dashboard-action-control.d.ts +25 -0
- package/dist/dashboard-action-control.js +73 -0
- package/dist/dashboard-action-routes.d.ts +9 -0
- package/dist/dashboard-action-routes.js +36 -0
- package/dist/dashboard-feature-control.d.ts +37 -0
- package/dist/dashboard-feature-control.js +206 -0
- package/dist/dashboard-module-projection.d.ts +9 -0
- package/dist/dashboard-module-projection.js +15 -0
- package/dist/dashboard-preview-import-routes.d.ts +6 -0
- package/dist/dashboard-preview-import-routes.js +121 -0
- package/dist/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +136 -2
- package/dist/discord-analytics.d.ts +3 -0
- package/dist/discord-analytics.js +49 -5
- package/dist/discord-auto-moderation-service.d.ts +21 -0
- package/dist/discord-auto-moderation-service.js +425 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-member-enforcement-service.d.ts +26 -0
- package/dist/discord-member-enforcement-service.js +590 -0
- package/dist/discord-message-enforcement-service.d.ts +9 -0
- package/dist/discord-message-enforcement-service.js +102 -0
- package/dist/discord-violation-thread-service.d.ts +33 -0
- package/dist/discord-violation-thread-service.js +789 -0
- package/dist/ecko-evidence-receiver.d.ts +32 -0
- package/dist/ecko-evidence-receiver.js +173 -0
- package/dist/ecko-transport-auth.d.ts +19 -0
- package/dist/ecko-transport-auth.js +80 -0
- package/dist/ecko-transport-routes.d.ts +19 -0
- package/dist/ecko-transport-routes.js +130 -0
- package/dist/embedded-auto-moderation-pool.d.ts +46 -0
- package/dist/embedded-auto-moderation-pool.js +324 -0
- package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
- package/dist/embedded-auto-moderation-scanner.js +263 -0
- package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
- package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
- package/dist/embedded-auto-moderation-worker.d.ts +2 -0
- package/dist/embedded-auto-moderation-worker.js +13 -0
- package/dist/events.d.ts +5 -1
- package/dist/events.js +275 -9
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +164 -5
- package/dist/hosted-server-moderation.d.ts +100 -0
- package/dist/hosted-server-moderation.js +226 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +101 -12
- package/dist/moderation-action-registry.d.ts +11 -0
- package/dist/moderation-action-registry.js +375 -0
- package/dist/moderation-case-disassociation.d.ts +13 -0
- package/dist/moderation-case-disassociation.js +83 -0
- package/dist/moderation-platform-services.d.ts +27 -0
- package/dist/moderation-platform-services.js +47 -0
- package/dist/modules.d.ts +6 -0
- package/dist/modules.js +65 -3
- package/dist/premium-activation-module.js +2 -0
- package/dist/privileged-message-metadata-cache.d.ts +35 -0
- package/dist/privileged-message-metadata-cache.js +70 -0
- package/dist/scheduled-task-runner.js +24 -1
- package/dist/shard-contracts.d.ts +151 -1
- package/dist/shard-contracts.js +142 -1
- package/dist/shard-coordinator.d.ts +1 -1
- package/dist/shard-ecko-scanner-registry.d.ts +38 -0
- package/dist/shard-ecko-scanner-registry.js +96 -0
- package/dist/shard-router.d.ts +23 -0
- package/dist/shard-router.js +69 -5
- package/dist/shard-supervisor.d.ts +11 -0
- package/dist/shard-supervisor.js +72 -6
- package/dist/shard-worker.d.ts +16 -1
- package/dist/shard-worker.js +108 -10
- package/dist/transient-violation-evidence.d.ts +39 -0
- package/dist/transient-violation-evidence.js +132 -0
- package/documentation/README.md +2 -0
- package/documentation/examples/.env.example +13 -0
- package/documentation/guides/self-hosting.md +17 -1
- package/documentation/modules/auto-moderation.md +51 -0
- package/documentation/modules/moderation.md +42 -0
- package/documentation/reference/environment.md +11 -0
- package/package.json +17 -5
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type DatabasePool, type HostedServerLeaveDeliveryOutcome } from "@helyx/database";
|
|
2
|
+
import type { Client, Guild } from "discord.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { InteractionLogger } from "./interactions.js";
|
|
5
|
+
declare const resultSchema: z.ZodObject<{
|
|
6
|
+
operationId: z.ZodUUID;
|
|
7
|
+
guildId: z.ZodString;
|
|
8
|
+
action: z.ZodEnum<{
|
|
9
|
+
leave: "leave";
|
|
10
|
+
blacklist: "blacklist";
|
|
11
|
+
}>;
|
|
12
|
+
outcome: z.ZodEnum<{
|
|
13
|
+
completed: "completed";
|
|
14
|
+
requires_review: "requires_review";
|
|
15
|
+
failed: "failed";
|
|
16
|
+
queued: "queued";
|
|
17
|
+
not_currently_installed: "not_currently_installed";
|
|
18
|
+
accepted_for_retry: "accepted_for_retry";
|
|
19
|
+
already_blacklisted: "already_blacklisted";
|
|
20
|
+
}>;
|
|
21
|
+
paidSlotsReleased: z.ZodNumber;
|
|
22
|
+
}, z.core.$loose>;
|
|
23
|
+
export interface DeveloperServerModeration {
|
|
24
|
+
moderate(input: {
|
|
25
|
+
action: "leave" | "blacklist";
|
|
26
|
+
guildId: string;
|
|
27
|
+
actorUserId: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
idempotencyKey: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
operationId: string;
|
|
32
|
+
outcome: z.infer<typeof resultSchema>["outcome"];
|
|
33
|
+
paidSlotsReleased: number;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare class HostedServerModerationClient implements DeveloperServerModeration {
|
|
37
|
+
private readonly apiUrl;
|
|
38
|
+
private readonly secret;
|
|
39
|
+
constructor(apiUrl: string, secret: string);
|
|
40
|
+
moderate(input: {
|
|
41
|
+
action: "leave" | "blacklist";
|
|
42
|
+
guildId: string;
|
|
43
|
+
actorUserId: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
idempotencyKey: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
operationId: string;
|
|
48
|
+
outcome: "completed" | "requires_review" | "failed" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
|
|
49
|
+
paidSlotsReleased: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export interface HostedBotInviteGate {
|
|
53
|
+
isEnabled(): Promise<boolean>;
|
|
54
|
+
}
|
|
55
|
+
export declare class HostedBotInviteGateClient implements HostedBotInviteGate {
|
|
56
|
+
private readonly apiUrl;
|
|
57
|
+
private readonly secret;
|
|
58
|
+
constructor(apiUrl: string, secret: string);
|
|
59
|
+
isEnabled(): Promise<boolean>;
|
|
60
|
+
}
|
|
61
|
+
export interface HostedGuildOwnerObserver {
|
|
62
|
+
observeOwner(guild: Guild): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export interface HostedGuildSafety extends HostedGuildOwnerObserver {
|
|
65
|
+
isBlacklisted(guildId: string): Promise<boolean>;
|
|
66
|
+
blacklistedGuildIds(guildIds: readonly string[]): Promise<Set<string>>;
|
|
67
|
+
recordAutomaticLeave(guildId: string): Promise<string>;
|
|
68
|
+
}
|
|
69
|
+
export interface HostedServerOwnerObservation {
|
|
70
|
+
username: string;
|
|
71
|
+
observedAt: string;
|
|
72
|
+
}
|
|
73
|
+
export declare class PostgresHostedGuildSafety implements HostedGuildSafety {
|
|
74
|
+
#private;
|
|
75
|
+
constructor(database: DatabasePool);
|
|
76
|
+
isBlacklisted(guildId: string): Promise<boolean>;
|
|
77
|
+
blacklistedGuildIds(guildIds: readonly string[]): Promise<Set<string>>;
|
|
78
|
+
recordAutomaticLeave(guildId: string): Promise<string>;
|
|
79
|
+
observeOwner(guild: Guild): Promise<void>;
|
|
80
|
+
refreshOwner(guild: Guild): Promise<HostedServerOwnerObservation>;
|
|
81
|
+
}
|
|
82
|
+
export declare class HostedServerLeaveWorker {
|
|
83
|
+
#private;
|
|
84
|
+
private readonly client;
|
|
85
|
+
private readonly logger;
|
|
86
|
+
private readonly shardScope?;
|
|
87
|
+
constructor(database: DatabasePool, client: Client, logger: InteractionLogger, shardScope?: {
|
|
88
|
+
shardId: number;
|
|
89
|
+
shardCount: number;
|
|
90
|
+
} | undefined);
|
|
91
|
+
start(): void;
|
|
92
|
+
runOnce(): Promise<void>;
|
|
93
|
+
deliverOperation(input: {
|
|
94
|
+
operationId: string;
|
|
95
|
+
guildId: string;
|
|
96
|
+
}): Promise<HostedServerLeaveDeliveryOutcome>;
|
|
97
|
+
stop(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=hosted-server-moderation.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { HostedServerSafetyRepository, } from "@helyx/database";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const resultSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
operationId: z.uuid(),
|
|
6
|
+
guildId: z.string().regex(/^\d{17,20}$/u),
|
|
7
|
+
action: z.enum(["leave", "blacklist"]),
|
|
8
|
+
outcome: z.enum([
|
|
9
|
+
"queued",
|
|
10
|
+
"completed",
|
|
11
|
+
"already_blacklisted",
|
|
12
|
+
"not_currently_installed",
|
|
13
|
+
"accepted_for_retry",
|
|
14
|
+
"requires_review",
|
|
15
|
+
"failed",
|
|
16
|
+
]),
|
|
17
|
+
paidSlotsReleased: z.number().int().nonnegative(),
|
|
18
|
+
})
|
|
19
|
+
.passthrough();
|
|
20
|
+
const botInviteGateSchema = z.object({ enabled: z.boolean() }).strict();
|
|
21
|
+
export class HostedServerModerationClient {
|
|
22
|
+
apiUrl;
|
|
23
|
+
secret;
|
|
24
|
+
constructor(apiUrl, secret) {
|
|
25
|
+
this.apiUrl = apiUrl;
|
|
26
|
+
this.secret = secret;
|
|
27
|
+
}
|
|
28
|
+
async moderate(input) {
|
|
29
|
+
const response = await fetch(new URL(`/v1/internal/hosted-server-moderation/${input.action}`, this.apiUrl), {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: `Bearer ${this.secret}`,
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
guildId: input.guildId,
|
|
37
|
+
actorUserId: input.actorUserId,
|
|
38
|
+
reason: input.reason,
|
|
39
|
+
idempotencyKey: input.idempotencyKey,
|
|
40
|
+
}),
|
|
41
|
+
signal: AbortSignal.timeout(10_000),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
throw new Error(`Hosted moderation request failed with ${response.status}`);
|
|
45
|
+
const result = resultSchema.parse(await response.json());
|
|
46
|
+
if (result.guildId !== input.guildId || result.action !== input.action)
|
|
47
|
+
throw new Error("Hosted moderation response did not match the request");
|
|
48
|
+
return {
|
|
49
|
+
operationId: result.operationId,
|
|
50
|
+
outcome: result.outcome,
|
|
51
|
+
paidSlotsReleased: result.paidSlotsReleased,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class HostedBotInviteGateClient {
|
|
56
|
+
apiUrl;
|
|
57
|
+
secret;
|
|
58
|
+
constructor(apiUrl, secret) {
|
|
59
|
+
this.apiUrl = apiUrl;
|
|
60
|
+
this.secret = secret;
|
|
61
|
+
}
|
|
62
|
+
async isEnabled() {
|
|
63
|
+
const response = await fetch(new URL("/v1/internal/incident-gates/bot-invites", this.apiUrl), {
|
|
64
|
+
headers: { authorization: `Bearer ${this.secret}` },
|
|
65
|
+
signal: AbortSignal.timeout(3_000),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
throw new Error(`Bot Invite gate request failed with ${response.status}`);
|
|
69
|
+
return botInviteGateSchema.parse(await response.json()).enabled;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class PostgresHostedGuildSafety {
|
|
73
|
+
#repository;
|
|
74
|
+
constructor(database) {
|
|
75
|
+
this.#repository = new HostedServerSafetyRepository(database);
|
|
76
|
+
}
|
|
77
|
+
async isBlacklisted(guildId) {
|
|
78
|
+
return (await this.#repository.getState(guildId)).blacklisted;
|
|
79
|
+
}
|
|
80
|
+
blacklistedGuildIds(guildIds) {
|
|
81
|
+
return this.#repository.getBlacklistedGuildIds(guildIds);
|
|
82
|
+
}
|
|
83
|
+
recordAutomaticLeave(guildId) {
|
|
84
|
+
return this.#repository.enqueueAutomaticLeave(guildId);
|
|
85
|
+
}
|
|
86
|
+
async observeOwner(guild) {
|
|
87
|
+
await this.refreshOwner(guild);
|
|
88
|
+
}
|
|
89
|
+
async refreshOwner(guild) {
|
|
90
|
+
const cached = guild.members.cache.get(guild.ownerId)?.user ??
|
|
91
|
+
guild.client.users.cache?.get(guild.ownerId);
|
|
92
|
+
const user = cached ?? (await guild.client.users.fetch(guild.ownerId));
|
|
93
|
+
const observedAt = new Date();
|
|
94
|
+
const persisted = await this.#repository.observeOwner({
|
|
95
|
+
guildId: guild.id,
|
|
96
|
+
ownerUserId: guild.ownerId,
|
|
97
|
+
username: user.username,
|
|
98
|
+
observedAt,
|
|
99
|
+
});
|
|
100
|
+
if (!persisted)
|
|
101
|
+
throw new Error("Guild owner observation no longer matched the installation");
|
|
102
|
+
return { username: user.username, observedAt: observedAt.toISOString() };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class HostedServerLeaveWorker {
|
|
106
|
+
client;
|
|
107
|
+
logger;
|
|
108
|
+
shardScope;
|
|
109
|
+
#repository;
|
|
110
|
+
#timer;
|
|
111
|
+
#active;
|
|
112
|
+
#stopped = false;
|
|
113
|
+
constructor(database, client, logger, shardScope) {
|
|
114
|
+
this.client = client;
|
|
115
|
+
this.logger = logger;
|
|
116
|
+
this.shardScope = shardScope;
|
|
117
|
+
this.#repository = new HostedServerSafetyRepository(database);
|
|
118
|
+
}
|
|
119
|
+
start() {
|
|
120
|
+
if (this.#timer || this.#stopped)
|
|
121
|
+
return;
|
|
122
|
+
this.#runSoon();
|
|
123
|
+
this.#timer = setInterval(() => this.#runSoon(), 15_000);
|
|
124
|
+
this.#timer.unref();
|
|
125
|
+
}
|
|
126
|
+
async runOnce() {
|
|
127
|
+
if (this.#stopped || this.#active)
|
|
128
|
+
return this.#active;
|
|
129
|
+
this.#active = this.#drain().finally(() => {
|
|
130
|
+
this.#active = undefined;
|
|
131
|
+
});
|
|
132
|
+
return this.#active;
|
|
133
|
+
}
|
|
134
|
+
async deliverOperation(input) {
|
|
135
|
+
if (this.#stopped)
|
|
136
|
+
return "queued";
|
|
137
|
+
await this.#active;
|
|
138
|
+
const claim = await this.#repository.claimLeaveDelivery({
|
|
139
|
+
...input,
|
|
140
|
+
leaseSeconds: 30,
|
|
141
|
+
...(this.shardScope ?? {}),
|
|
142
|
+
});
|
|
143
|
+
if (claim)
|
|
144
|
+
return this.#deliver(claim);
|
|
145
|
+
return ((await this.#repository.getLeaveDeliveryOutcome(input.operationId, input.guildId)) ?? "queued");
|
|
146
|
+
}
|
|
147
|
+
async stop() {
|
|
148
|
+
this.#stopped = true;
|
|
149
|
+
if (this.#timer)
|
|
150
|
+
clearInterval(this.#timer);
|
|
151
|
+
this.#timer = undefined;
|
|
152
|
+
await this.#active;
|
|
153
|
+
}
|
|
154
|
+
async #drain() {
|
|
155
|
+
const claims = await this.#repository.claimLeaveDeliveries({
|
|
156
|
+
limit: 10,
|
|
157
|
+
leaseSeconds: 30,
|
|
158
|
+
...(this.shardScope ?? {}),
|
|
159
|
+
});
|
|
160
|
+
await Promise.all(claims.map((claim) => this.#deliver(claim)));
|
|
161
|
+
}
|
|
162
|
+
#runSoon() {
|
|
163
|
+
void this.runOnce().catch(() => {
|
|
164
|
+
this.logger.error({
|
|
165
|
+
event: "hosted_moderation.leave_claim_failed",
|
|
166
|
+
reasonCode: "database_unavailable",
|
|
167
|
+
}, "Hosted server leave claims are temporarily unavailable");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async #deliver(claim) {
|
|
171
|
+
const guild = this.client.guilds.cache.get(claim.guildId);
|
|
172
|
+
if (!guild) {
|
|
173
|
+
await this.#repository.completeLeaveDelivery(claim, "not_currently_installed");
|
|
174
|
+
return "not_currently_installed";
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await guild.leave();
|
|
178
|
+
await this.#repository.completeLeaveDelivery(claim, "completed");
|
|
179
|
+
this.logger.info({
|
|
180
|
+
event: "hosted_moderation.leave_completed",
|
|
181
|
+
guildId: claim.guildId,
|
|
182
|
+
operationId: claim.operationId,
|
|
183
|
+
}, "Hosted server leave completed");
|
|
184
|
+
return "completed";
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const failureCode = classifyLeaveFailure(error);
|
|
188
|
+
if (failureCode === "discord_not_installed") {
|
|
189
|
+
await this.#repository.completeLeaveDelivery(claim, "not_currently_installed");
|
|
190
|
+
return "not_currently_installed";
|
|
191
|
+
}
|
|
192
|
+
const outcome = await this.#repository.failLeaveDelivery({
|
|
193
|
+
claim,
|
|
194
|
+
failureCode,
|
|
195
|
+
retryAfterSeconds: Math.min(3_600, 15 * 2 ** claim.attemptCount),
|
|
196
|
+
});
|
|
197
|
+
this.logger.warn({
|
|
198
|
+
event: "hosted_moderation.leave_deferred",
|
|
199
|
+
guildId: claim.guildId,
|
|
200
|
+
operationId: claim.operationId,
|
|
201
|
+
failureCode,
|
|
202
|
+
outcome,
|
|
203
|
+
}, "Hosted server leave will be retried or reviewed");
|
|
204
|
+
return outcome === "requires_review"
|
|
205
|
+
? outcome
|
|
206
|
+
: outcome === "retry"
|
|
207
|
+
? "accepted_for_retry"
|
|
208
|
+
: "queued";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function classifyLeaveFailure(error) {
|
|
213
|
+
if (typeof error !== "object" || error === null)
|
|
214
|
+
return "discord_transient";
|
|
215
|
+
const candidate = error;
|
|
216
|
+
const code = candidate.code;
|
|
217
|
+
if (code === 10_004 || code === 50_001)
|
|
218
|
+
return "discord_not_installed";
|
|
219
|
+
const status = candidate.status;
|
|
220
|
+
if (status === 401 || status === 403)
|
|
221
|
+
return "discord_authentication";
|
|
222
|
+
if (status === 429)
|
|
223
|
+
return "discord_rate_limited";
|
|
224
|
+
return "discord_transient";
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=hosted-server-moderation.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export interface BotHandle {
|
|
|
11
11
|
export declare function startBot(processEnvironment?: NodeJS.ProcessEnv): Promise<BotHandle>;
|
|
12
12
|
export declare function resolveBotWorkerEntryPoint(entryUrl?: string): string;
|
|
13
13
|
export declare function readBotVersion(): string;
|
|
14
|
-
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean, guildMembersEnabled?: boolean): GatewayIntentBits[];
|
|
14
|
+
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean, guildMembersEnabled?: boolean, runtimeProfile?: "hosted" | "self-hosted"): GatewayIntentBits[];
|
|
15
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { StatusHeartbeatReporter } from "@helyx/status-client";
|
|
3
3
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
4
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, PremiumAllocationRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
5
5
|
import { createRuntime } from "@helyx/framework";
|
|
6
6
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
|
|
7
7
|
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
@@ -19,9 +19,10 @@ import { loadConfiguredModulePackages, platformServiceOptions, } from "./modules
|
|
|
19
19
|
import { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
20
20
|
import { createPermissionsModule } from "./permissions-module.js";
|
|
21
21
|
import { createManagementModule } from "./management-module.js";
|
|
22
|
-
import { DiscordMessageDeliveryService,
|
|
22
|
+
import { DiscordMessageDeliveryService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
23
|
+
import { DiscordModerationMemberEnforcementAdapter } from "./discord-member-enforcement-service.js";
|
|
23
24
|
import { DiscordModuleLoggingService } from "./module-logging.js";
|
|
24
|
-
import { BotControlService, createBotControlServer, DiscordGuildResourceDirectory, } from "./control-server.js";
|
|
25
|
+
import { BotControlService, BotControlTransportError, createBotControlServer, DiscordGuildResourceDirectory, } from "./control-server.js";
|
|
25
26
|
import { HttpVerificationService } from "./verification-service.js";
|
|
26
27
|
import { startShardSupervisor } from "./shard-supervisor.js";
|
|
27
28
|
import { parseShardWorkerLaunchContract, startShardWorker, } from "./shard-worker.js";
|
|
@@ -30,6 +31,9 @@ import { shardHealthSnapshotSchema, } from "./shard-contracts.js";
|
|
|
30
31
|
import { createSupportModule } from "./support-module.js";
|
|
31
32
|
import { createOAuthModule } from "./oauth-module.js";
|
|
32
33
|
import { createPrivacyModule } from "./privacy-module.js";
|
|
34
|
+
import { installModerationPlatformServices } from "./moderation-platform-services.js";
|
|
35
|
+
import { EckoEvidenceReceiver } from "./ecko-evidence-receiver.js";
|
|
36
|
+
import { EckoTransportAuthenticator } from "./ecko-transport-auth.js";
|
|
33
37
|
import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
|
|
34
38
|
import { createPremiumActivationModule } from "./premium-activation-module.js";
|
|
35
39
|
import { DiscordPremiumGuildAuthority } from "./premium-guild-authority.js";
|
|
@@ -50,6 +54,7 @@ import { discordRestOptions } from "./discord-rest.js";
|
|
|
50
54
|
import { discordBotStatusService } from "./status-producer.js";
|
|
51
55
|
import { assertReviewedPremiumCatalogue, requirePremiumProviderEnvironment, } from "./premium-access.js";
|
|
52
56
|
import { enforcePremiumCatalogue, PREMIUM_CATALOGUE_ENFORCEMENT_INTERVAL_MS, } from "./premium-catalogue-enforcement.js";
|
|
57
|
+
import { HostedBotInviteGateClient, HostedServerLeaveWorker, HostedServerModerationClient, PostgresHostedGuildSafety, } from "./hosted-server-moderation.js";
|
|
53
58
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
54
59
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
55
60
|
export * from "./control-server.js";
|
|
@@ -119,6 +124,17 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
119
124
|
sslMode: environment.DATABASE_SSL,
|
|
120
125
|
applicationName: "helyx-bot",
|
|
121
126
|
});
|
|
127
|
+
const hostedOwnerObserver = new PostgresHostedGuildSafety(database);
|
|
128
|
+
const eckoOperations = new EckoOperationsRepository(database);
|
|
129
|
+
const hostedSafety = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
130
|
+
? hostedOwnerObserver
|
|
131
|
+
: undefined;
|
|
132
|
+
const developerModeration = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
133
|
+
? new HostedServerModerationClient(environment.HELYX_CLOUD_API_URL, environment.HELYX_BOT_CONTROL_SECRET)
|
|
134
|
+
: undefined;
|
|
135
|
+
const hostedInviteGate = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
136
|
+
? new HostedBotInviteGateClient(environment.HELYX_CLOUD_API_URL, environment.HELYX_BOT_CONTROL_SECRET)
|
|
137
|
+
: undefined;
|
|
122
138
|
const privacyDatabase = environment.HELYX_PRIVACY_ENABLED &&
|
|
123
139
|
environment.HELYX_PRIVACY_EXECUTOR_DATABASE_URL &&
|
|
124
140
|
environment.HELYX_PRIVACY_EXECUTOR_DATABASE_SSL
|
|
@@ -153,11 +169,12 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
153
169
|
verifiedMigrationCount: migrationResult.verified.length,
|
|
154
170
|
}, "Core database ready");
|
|
155
171
|
const cleanupExpiredCoreRecords = async () => {
|
|
156
|
-
const [sessions, rateLimits, verificationAudit, commandRecords] = await Promise.all([
|
|
172
|
+
const [sessions, rateLimits, verificationAudit, commandRecords, hostedLeaveDeliveries,] = await Promise.all([
|
|
157
173
|
new InteractionSessionRepository(database).deleteExpired(),
|
|
158
174
|
new RateLimitRepository(database).deleteExpired(),
|
|
159
175
|
new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
|
|
160
176
|
new GuildCommandRepository(database).deleteExpired(12),
|
|
177
|
+
new HostedServerSafetyRepository(database).deleteTerminalLeaveDeliveriesOlderThanMonths(24),
|
|
161
178
|
]);
|
|
162
179
|
const commandRecordCount = commandRecords.history +
|
|
163
180
|
commandRecords.idempotencyKeys +
|
|
@@ -166,13 +183,15 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
166
183
|
if (sessions > 0 ||
|
|
167
184
|
rateLimits > 0 ||
|
|
168
185
|
verificationAudit > 0 ||
|
|
169
|
-
commandRecordCount > 0
|
|
186
|
+
commandRecordCount > 0 ||
|
|
187
|
+
hostedLeaveDeliveries > 0) {
|
|
170
188
|
bootstrapLogger.info({
|
|
171
189
|
event: "database.retention_cleaned",
|
|
172
190
|
interactionSessions: sessions,
|
|
173
191
|
rateLimitWindows: rateLimits,
|
|
174
192
|
verificationAuditEvents: verificationAudit,
|
|
175
193
|
commandRecords: commandRecordCount,
|
|
194
|
+
hostedLeaveDeliveries,
|
|
176
195
|
}, "Expired core records removed");
|
|
177
196
|
}
|
|
178
197
|
};
|
|
@@ -213,7 +232,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
213
232
|
premiumCatalogueTimer.unref();
|
|
214
233
|
const client = new Client({
|
|
215
234
|
rest: discordRestOptions,
|
|
216
|
-
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
235
|
+
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED, environment.HELYX_RUNTIME_PROFILE),
|
|
217
236
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
218
237
|
allowedMentions: { parse: [] },
|
|
219
238
|
});
|
|
@@ -339,6 +358,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
339
358
|
health: {
|
|
340
359
|
getHealth: buildHealth,
|
|
341
360
|
},
|
|
361
|
+
...(developerModeration ? { moderation: developerModeration } : {}),
|
|
342
362
|
})
|
|
343
363
|
: undefined;
|
|
344
364
|
const modules = [
|
|
@@ -376,11 +396,26 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
376
396
|
platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
|
|
377
397
|
platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
|
|
378
398
|
platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
|
|
379
|
-
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new
|
|
399
|
+
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordModerationMemberEnforcementAdapter(client));
|
|
380
400
|
const memberRoles = new DiscordMemberRoleService(client);
|
|
381
401
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, memberRoles);
|
|
382
402
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoleGrants, new PlatformMemberRoleGrantOwnershipService(new PostgresMemberRoleGrantOwnershipStore(database), memberRoles));
|
|
383
403
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
404
|
+
const moderationPlatformServices = installModerationPlatformServices({
|
|
405
|
+
services: platformServices,
|
|
406
|
+
serviceAccess,
|
|
407
|
+
client,
|
|
408
|
+
modules: publicModules,
|
|
409
|
+
runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
|
|
410
|
+
deploymentEnvironment: environment.HELYX_DEPLOYMENT_ENVIRONMENT,
|
|
411
|
+
embedded: {
|
|
412
|
+
workers: environment.HELYX_AUTOMOD_EMBEDDED_WORKERS,
|
|
413
|
+
maximumItems: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS,
|
|
414
|
+
maximumBytes: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES,
|
|
415
|
+
maximumItemsPerGuild: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD,
|
|
416
|
+
refreshIntervalMs: environment.HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS * 1_000,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
384
419
|
platformServices.set(HELYX_SERVICE_NAMES.discordGuildHealth, new DiscordGuildHealthInspection(client, publicModules, serviceAccess));
|
|
385
420
|
platformServices.set(HELYX_SERVICE_NAMES.discordAnalytics, new DiscordAnalyticsInspection(client));
|
|
386
421
|
if (serviceOptions.inviteFingerprintKey)
|
|
@@ -402,7 +437,8 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
402
437
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
403
438
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
404
439
|
]);
|
|
405
|
-
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, undefined, environment.DISCORD_CLIENT_ID);
|
|
440
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, undefined, environment.DISCORD_CLIENT_ID, hostedSafety, hostedInviteGate, hostedOwnerObserver);
|
|
441
|
+
const hostedLeaveWorker = new HostedServerLeaveWorker(database, client, bootstrapLogger);
|
|
406
442
|
const commandsReady = installInteractions(client, modules, {
|
|
407
443
|
globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
|
|
408
444
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
@@ -419,7 +455,10 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
419
455
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
420
456
|
]),
|
|
421
457
|
}, bootstrapLogger, serviceAccess);
|
|
422
|
-
const moduleEvents = installModuleEvents(client, publicModules, bootstrapLogger, serviceAccess
|
|
458
|
+
const moduleEvents = installModuleEvents(client, publicModules, bootstrapLogger, serviceAccess, {
|
|
459
|
+
runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
|
|
460
|
+
evidenceStore: moderationPlatformServices.evidenceStore,
|
|
461
|
+
});
|
|
423
462
|
const controlService = new BotControlService(database, [
|
|
424
463
|
...publicModules,
|
|
425
464
|
gamingStatisticsRecordOwner,
|
|
@@ -449,6 +488,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
449
488
|
return;
|
|
450
489
|
shuttingDown = true;
|
|
451
490
|
prepareGuilds.stop();
|
|
491
|
+
await hostedLeaveWorker?.stop();
|
|
452
492
|
process.off("SIGINT", onSigint);
|
|
453
493
|
process.off("SIGTERM", onSigterm);
|
|
454
494
|
if (retentionCleanupTimer)
|
|
@@ -480,6 +520,12 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
480
520
|
process.exitCode = 1;
|
|
481
521
|
bootstrapLogger.error({ event: "module_events.stop_failed", error: safeError(error) }, "Module event shutdown failed");
|
|
482
522
|
});
|
|
523
|
+
await moderationPlatformServices.embeddedScanner
|
|
524
|
+
?.stop()
|
|
525
|
+
.catch((error) => {
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
bootstrapLogger.error({ event: "automod.embedded_stop_failed", error: safeError(error) }, "Embedded Auto Moderation scanner shutdown failed");
|
|
528
|
+
});
|
|
483
529
|
await runtime.stop().catch((error) => {
|
|
484
530
|
process.exitCode = 1;
|
|
485
531
|
bootstrapLogger.error({ event: "runtime.stop_failed", error: safeError(error) }, "Runtime shutdown failed");
|
|
@@ -511,8 +557,14 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
511
557
|
process.once("SIGTERM", onSigterm);
|
|
512
558
|
try {
|
|
513
559
|
await runtime.start();
|
|
560
|
+
await moderationPlatformServices.embeddedScanner
|
|
561
|
+
?.synchronize()
|
|
562
|
+
.catch((error) => {
|
|
563
|
+
bootstrapLogger.warn({ event: "automod.embedded_sync_failed", error: safeError(error) }, "Embedded Auto Moderation scanner started in Standard fallback");
|
|
564
|
+
});
|
|
514
565
|
await client.login(environment.DISCORD_TOKEN);
|
|
515
566
|
await commandsReady;
|
|
567
|
+
hostedLeaveWorker?.start();
|
|
516
568
|
scheduledTasks = createScheduledTaskRunner({
|
|
517
569
|
database,
|
|
518
570
|
modules: publicModules,
|
|
@@ -565,11 +617,38 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
565
617
|
}
|
|
566
618
|
await cloudConnector.start();
|
|
567
619
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
620
|
+
const ecko = environment.HELYX_ECKO_TRANSPORT_SECRET
|
|
621
|
+
? {
|
|
622
|
+
authenticator: new EckoTransportAuthenticator(environment.HELYX_DEPLOYMENT_ENVIRONMENT, [
|
|
623
|
+
environment.HELYX_ECKO_TRANSPORT_SECRET,
|
|
624
|
+
...(environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET
|
|
625
|
+
? [environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET]
|
|
626
|
+
: []),
|
|
627
|
+
]),
|
|
628
|
+
registry: moderationPlatformServices.scannerRegistry,
|
|
629
|
+
evidence: new EckoEvidenceReceiver(moderationPlatformServices.evidenceStore, environment.HELYX_DEPLOYMENT_ENVIRONMENT, Buffer.from(environment.HELYX_ECKO_TRANSPORT_SECRET, "base64")),
|
|
630
|
+
gateState: () => eckoOperations.readGate(),
|
|
631
|
+
telemetry: {
|
|
632
|
+
accept: (batch) => eckoOperations.acceptTelemetry(batch),
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
: undefined;
|
|
568
636
|
controlServer = await createBotControlServer({
|
|
569
637
|
service: controlService,
|
|
570
638
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
639
|
+
deliverHostedServerLeave: (input) => hostedLeaveWorker.deliverOperation(input),
|
|
640
|
+
refreshHostedServerOwner: async (guildId) => {
|
|
641
|
+
const guild = client.guilds.cache.get(guildId);
|
|
642
|
+
if (!guild)
|
|
643
|
+
throw new BotControlTransportError(404, "The hosted server is not installed");
|
|
644
|
+
return hostedOwnerObserver.refreshOwner(guild);
|
|
645
|
+
},
|
|
571
646
|
logger: { level: environment.LOG_LEVEL },
|
|
647
|
+
runtimeConfiguration: {
|
|
648
|
+
hostedModerationEnabled: environment.HELYX_HOSTED_MODERATION_ENABLED,
|
|
649
|
+
},
|
|
572
650
|
guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", modules),
|
|
651
|
+
...(ecko ? { ecko } : {}),
|
|
573
652
|
});
|
|
574
653
|
await controlServer.listen({
|
|
575
654
|
host: "::",
|
|
@@ -648,7 +727,7 @@ function deploymentReference(processEnvironment) {
|
|
|
648
727
|
processEnvironment.GIT_COMMIT_SHA;
|
|
649
728
|
return value?.trim().slice(0, 12) || undefined;
|
|
650
729
|
}
|
|
651
|
-
export function gatewayIntentsForModules(modules, messageContentEnabled = false, guildMembersEnabled = false) {
|
|
730
|
+
export function gatewayIntentsForModules(modules, messageContentEnabled = false, guildMembersEnabled = false, runtimeProfile = "hosted") {
|
|
652
731
|
const intents = [GatewayIntentBits.Guilds];
|
|
653
732
|
if (modules.some((module) => module.events?.messageReactionAdd?.length ||
|
|
654
733
|
module.events?.messageReactionRemove?.length ||
|
|
@@ -656,8 +735,12 @@ export function gatewayIntentsForModules(modules, messageContentEnabled = false,
|
|
|
656
735
|
module.events?.messageReactionRemoveEmoji?.length)) {
|
|
657
736
|
intents.push(GatewayIntentBits.GuildMessageReactions);
|
|
658
737
|
}
|
|
659
|
-
const hasPrivilegedMessageCreate = modules.some((module) => module.events?.privilegedMessageCreate?.
|
|
660
|
-
|
|
738
|
+
const hasPrivilegedMessageCreate = modules.some((module) => module.events?.privilegedMessageCreate?.some(({ featureId }) => !featureId || runtimeProfile === "self-hosted") ||
|
|
739
|
+
module.events?.privilegedMessageUpdate?.some(({ featureId }) => !featureId || runtimeProfile === "self-hosted"));
|
|
740
|
+
const requiresMessageContent = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content") ||
|
|
741
|
+
(runtimeProfile === "self-hosted" &&
|
|
742
|
+
module.manifest.manifestVersion === "0.5" &&
|
|
743
|
+
module.manifest.features.some((feature) => feature.requiredDiscordCapabilities.includes("message-content"))));
|
|
661
744
|
if (((hasPrivilegedMessageCreate || requiresMessageContent) &&
|
|
662
745
|
messageContentEnabled) ||
|
|
663
746
|
modules.some((module) => module.events?.guildMessageCreate?.length ||
|
|
@@ -679,6 +762,12 @@ export function gatewayIntentsForModules(modules, messageContentEnabled = false,
|
|
|
679
762
|
if (modules.some((module) => module.events?.guildVoiceSession?.length)) {
|
|
680
763
|
intents.push(GatewayIntentBits.GuildVoiceStates);
|
|
681
764
|
}
|
|
765
|
+
if (modules.some((module) => module.events?.discordAutoModerationRuleCreate?.length ||
|
|
766
|
+
module.events?.discordAutoModerationRuleUpdate?.length ||
|
|
767
|
+
module.events?.discordAutoModerationRuleDelete?.length))
|
|
768
|
+
intents.push(GatewayIntentBits.AutoModerationConfiguration);
|
|
769
|
+
if (modules.some((module) => module.events?.discordAutoModerationActionExecution?.length))
|
|
770
|
+
intents.push(GatewayIntentBits.AutoModerationExecution);
|
|
682
771
|
if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
|
|
683
772
|
module.events?.guildEmojiUpdate?.length)) {
|
|
684
773
|
intents.push(GatewayIntentBits.GuildExpressions);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModerationActionRegistryService, ModerationActionResultV2, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import type { DiscordViolationEvidenceDeliveryAdapter } from "./discord-violation-thread-service.js";
|
|
3
|
+
/** Local closed registry. In sharded mode it is instantiated in each guild owner. */
|
|
4
|
+
export declare class BotModerationActionRegistry implements ModerationActionRegistryService {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly services;
|
|
7
|
+
private readonly evidenceDelivery?;
|
|
8
|
+
constructor(modules: readonly ModuleDefinition[], services: ServiceAccess, evidenceDelivery?: DiscordViolationEvidenceDeliveryAdapter | undefined);
|
|
9
|
+
execute(input: Parameters<ModerationActionRegistryService["execute"]>[0]): Promise<ModerationActionResultV2>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=moderation-action-registry.d.ts.map
|