@helyx/bot 1.1.0 → 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 +62 -0
- package/README.md +16 -1
- 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/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 +243 -4
- package/dist/guilds.d.ts +2 -2
- package/dist/guilds.js +14 -8
- package/dist/hosted-server-moderation.d.ts +16 -5
- package/dist/hosted-server-moderation.js +32 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -14
- 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/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 +67 -4
- package/dist/shard-worker.d.ts +15 -1
- package/dist/shard-worker.js +95 -17
- 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 +12 -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,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
3
|
+
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
4
|
+
const actionParameters = z.object({
|
|
5
|
+
guildId: snowflake,
|
|
6
|
+
moduleId,
|
|
7
|
+
actionId: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
|
|
8
|
+
});
|
|
9
|
+
const mutationContext = z.object({
|
|
10
|
+
actorUserId: snowflake,
|
|
11
|
+
correlationId: z.uuid(),
|
|
12
|
+
});
|
|
13
|
+
export function registerDashboardActionRoutes(server, service) {
|
|
14
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/actions/:actionId/preview", async (request) => {
|
|
15
|
+
const params = actionParameters.parse(request.params);
|
|
16
|
+
const body = mutationContext.parse(request.body);
|
|
17
|
+
return service.previewDashboardAction({ ...params, ...body });
|
|
18
|
+
});
|
|
19
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/actions/:actionId", async (request) => {
|
|
20
|
+
const params = actionParameters.parse(request.params);
|
|
21
|
+
const body = mutationContext
|
|
22
|
+
.extend({
|
|
23
|
+
inspectionRevision: z.string().min(1).max(200).optional(),
|
|
24
|
+
})
|
|
25
|
+
.parse(request.body);
|
|
26
|
+
return service.executeDashboardAction({
|
|
27
|
+
...params,
|
|
28
|
+
actorUserId: body.actorUserId,
|
|
29
|
+
correlationId: body.correlationId,
|
|
30
|
+
...(body.inspectionRevision === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { inspectionRevision: body.inspectionRevision }),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=dashboard-action-routes.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DashboardResourceField, DashboardResourceScalarField, DashboardSetting } from "@helyx/module-manifest";
|
|
2
|
+
import { type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type ModuleConfigurationService, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
3
|
+
export declare function requireDashboardFeatures(services: ServiceAccess, guildId: string, moduleId: string, featureIds: readonly (string | undefined)[]): Promise<void>;
|
|
4
|
+
export declare function changedSettingFeatureIds(settings: readonly DashboardSetting[], current: Readonly<Record<string, unknown>>, next: Readonly<Record<string, unknown>>): readonly string[];
|
|
5
|
+
export declare function scalarInputFeatureIds(inputs: readonly DashboardResourceScalarField[], value: Readonly<Record<string, ManagedResourceValue>>): readonly string[];
|
|
6
|
+
export declare function configurationPreviewFeatureIds(configurations: ModuleConfigurationService, guildId: string, module: ModuleDefinition, preview: {
|
|
7
|
+
featureId?: string | undefined;
|
|
8
|
+
inputs: readonly DashboardResourceScalarField[];
|
|
9
|
+
}, input: {
|
|
10
|
+
value: Readonly<Record<string, unknown>>;
|
|
11
|
+
input: Readonly<Record<string, ManagedResourceValue>>;
|
|
12
|
+
}): Promise<readonly (string | undefined)[]>;
|
|
13
|
+
export declare function resourceFieldFeatureIds(fields: readonly DashboardResourceField[], value: Readonly<Record<string, ManagedResourceValue>>): readonly string[];
|
|
14
|
+
export declare function validateManagedResourceInput(inputs: readonly DashboardResourceScalarField[], value: Readonly<Record<string, ManagedResourceValue>>): void;
|
|
15
|
+
export declare function isBoundedManagedResourceValue(value: unknown, depth?: number, budget?: {
|
|
16
|
+
keys: number;
|
|
17
|
+
}): value is ManagedResourceValue;
|
|
18
|
+
export declare function hasBoundedManagedResourceBytes(value: unknown): boolean;
|
|
19
|
+
export declare function normalizeManagedResourceDetail(value: Omit<ManagedResourceDetail, "description" | "attributes"> & {
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
attributes?: Readonly<Record<string, ManagedResourceValue>> | undefined;
|
|
22
|
+
}, attributeKeys: readonly string[]): ManagedResourceDetail;
|
|
23
|
+
export declare function normalizeManagedResourcePage(value: {
|
|
24
|
+
items: readonly (Omit<ManagedResourcePage["items"][number], "description" | "attributes"> & {
|
|
25
|
+
description?: string | undefined;
|
|
26
|
+
attributes?: Readonly<Record<string, ManagedResourceValue>> | undefined;
|
|
27
|
+
})[];
|
|
28
|
+
nextCursor?: string | undefined;
|
|
29
|
+
total?: number | undefined;
|
|
30
|
+
}, attributeKeys: readonly string[]): ManagedResourcePage;
|
|
31
|
+
export declare function normalizeManagedResourceRelationPage(value: {
|
|
32
|
+
items: ManagedResourceRelationPage["items"];
|
|
33
|
+
nextCursor?: string | undefined;
|
|
34
|
+
}, valueKeys: readonly string[]): ManagedResourceRelationPage;
|
|
35
|
+
export declare function projectManagedResourceObject(value: Readonly<Record<string, ManagedResourceValue>>, declaredKeys: readonly string[]): Record<string, ManagedResourceValue>;
|
|
36
|
+
export declare function managedResourceColumnKeys(module: ModuleDefinition, resourceType: string): readonly string[];
|
|
37
|
+
//# sourceMappingURL=dashboard-feature-control.d.ts.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { DashboardActionValidationError, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { BotControlForbiddenError } from "./control-errors.js";
|
|
3
|
+
const SNOWFLAKE_PATTERN = /^\d{17,20}$/u;
|
|
4
|
+
export async function requireDashboardFeatures(services, guildId, moduleId, featureIds) {
|
|
5
|
+
const unique = [...new Set(featureIds.filter(isDefined))];
|
|
6
|
+
if (unique.length === 0)
|
|
7
|
+
return;
|
|
8
|
+
if (!services.has(HELYX_SERVICE_NAMES.featureAccess))
|
|
9
|
+
throw new BotControlForbiddenError();
|
|
10
|
+
const access = services.get(HELYX_SERVICE_NAMES.featureAccess);
|
|
11
|
+
const decisions = await Promise.all(unique.map((featureId) => access.check({ guildId, moduleId, featureId }))).catch(() => []);
|
|
12
|
+
if (decisions.length !== unique.length ||
|
|
13
|
+
decisions.some(({ decision }) => decision !== "granted"))
|
|
14
|
+
throw new BotControlForbiddenError();
|
|
15
|
+
}
|
|
16
|
+
export function changedSettingFeatureIds(settings, current, next) {
|
|
17
|
+
return settings.flatMap((setting) => {
|
|
18
|
+
if (!Object.hasOwn(next, setting.key))
|
|
19
|
+
return [];
|
|
20
|
+
const changed = !configurationValuesEqual(Object.hasOwn(current, setting.key)
|
|
21
|
+
? current[setting.key]
|
|
22
|
+
: "default" in setting
|
|
23
|
+
? setting.default
|
|
24
|
+
: undefined, next[setting.key]);
|
|
25
|
+
if (!changed)
|
|
26
|
+
return [];
|
|
27
|
+
const selectedFeatureId = setting.type === "select" && typeof next[setting.key] === "string"
|
|
28
|
+
? setting.options.find(({ value }) => value === next[setting.key])
|
|
29
|
+
?.featureId
|
|
30
|
+
: undefined;
|
|
31
|
+
return [setting.featureId, selectedFeatureId].filter(isDefined);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function scalarInputFeatureIds(inputs, value) {
|
|
35
|
+
return inputs.flatMap((input) => input.featureId && Object.hasOwn(value, input.key) ? [input.featureId] : []);
|
|
36
|
+
}
|
|
37
|
+
export async function configurationPreviewFeatureIds(configurations, guildId, module, preview, input) {
|
|
38
|
+
const current = await configurations.get(guildId, module.manifest.id);
|
|
39
|
+
return [
|
|
40
|
+
preview.featureId,
|
|
41
|
+
...scalarInputFeatureIds(preview.inputs, input.input),
|
|
42
|
+
...(module.manifest.manifestVersion === "0.5"
|
|
43
|
+
? changedSettingFeatureIds(module.manifest.dashboard?.settings ?? [], current?.value ?? {}, input.value)
|
|
44
|
+
: []),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
export function resourceFieldFeatureIds(fields, value) {
|
|
48
|
+
const featureIds = [];
|
|
49
|
+
for (const field of fields) {
|
|
50
|
+
const item = value[field.key];
|
|
51
|
+
if (item === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
if (field.featureId &&
|
|
54
|
+
(!("default" in field) || !configurationValuesEqual(field.default, item)))
|
|
55
|
+
featureIds.push(field.featureId);
|
|
56
|
+
if (field.type !== "list" || !Array.isArray(item))
|
|
57
|
+
continue;
|
|
58
|
+
for (const entry of item) {
|
|
59
|
+
if (!isRecord(entry))
|
|
60
|
+
continue;
|
|
61
|
+
featureIds.push(...scalarInputFeatureIds(field.fields, entry));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return featureIds;
|
|
65
|
+
}
|
|
66
|
+
export function validateManagedResourceInput(inputs, value) {
|
|
67
|
+
const declared = new Map(inputs.map((input) => [input.key, input]));
|
|
68
|
+
for (const key of Object.keys(value))
|
|
69
|
+
if (!declared.has(key))
|
|
70
|
+
throw new DashboardActionValidationError(`The action value "${key}" is not declared by the module.`);
|
|
71
|
+
for (const input of inputs) {
|
|
72
|
+
const item = value[input.key];
|
|
73
|
+
if (item === undefined) {
|
|
74
|
+
if (input.required)
|
|
75
|
+
throw new DashboardActionValidationError(`${input.label} is required for this action.`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const valid = input.type === "boolean"
|
|
79
|
+
? typeof item === "boolean"
|
|
80
|
+
: input.type === "integer"
|
|
81
|
+
? typeof item === "number" &&
|
|
82
|
+
Number.isInteger(item) &&
|
|
83
|
+
item >= input.minimum &&
|
|
84
|
+
item <= input.maximum &&
|
|
85
|
+
(item - input.minimum) % (input.step ?? 1) === 0
|
|
86
|
+
: input.type === "select"
|
|
87
|
+
? typeof item === "string" &&
|
|
88
|
+
input.options.some(({ value: option }) => option === item)
|
|
89
|
+
: input.type === "discord-channel" || input.type === "discord-role"
|
|
90
|
+
? isValidDiscordResourceSelection(input, item)
|
|
91
|
+
: typeof item === "string" &&
|
|
92
|
+
item.length >= input.minLength &&
|
|
93
|
+
item.length <= input.maxLength;
|
|
94
|
+
if (!valid)
|
|
95
|
+
throw new DashboardActionValidationError(`${input.label} is not a valid action value.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isValidDiscordResourceSelection(input, value) {
|
|
99
|
+
if (input.multiple !== true)
|
|
100
|
+
return typeof value === "string" && SNOWFLAKE_PATTERN.test(value);
|
|
101
|
+
if (!Array.isArray(value))
|
|
102
|
+
return false;
|
|
103
|
+
const minimum = input.minItems ?? (input.required ? 1 : 0);
|
|
104
|
+
const maximum = input.maxItems ?? 25;
|
|
105
|
+
return (value.length >= minimum &&
|
|
106
|
+
value.length <= maximum &&
|
|
107
|
+
new Set(value).size === value.length &&
|
|
108
|
+
value.every((item) => typeof item === "string" && SNOWFLAKE_PATTERN.test(item)));
|
|
109
|
+
}
|
|
110
|
+
export function isBoundedManagedResourceValue(value, depth = 0, budget = { keys: 0 }) {
|
|
111
|
+
if (depth > 8)
|
|
112
|
+
return false;
|
|
113
|
+
if (value === null ||
|
|
114
|
+
typeof value === "boolean" ||
|
|
115
|
+
(typeof value === "number" && Number.isFinite(value)))
|
|
116
|
+
return true;
|
|
117
|
+
if (typeof value === "string")
|
|
118
|
+
return value.length <= 4_000;
|
|
119
|
+
if (Array.isArray(value))
|
|
120
|
+
return (value.length <= 100 &&
|
|
121
|
+
value.every((item) => isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
122
|
+
if (!isRecord(value))
|
|
123
|
+
return false;
|
|
124
|
+
const prototype = Object.getPrototypeOf(value);
|
|
125
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
126
|
+
return false;
|
|
127
|
+
const entries = Object.entries(value);
|
|
128
|
+
budget.keys += entries.length;
|
|
129
|
+
return (entries.length <= 100 &&
|
|
130
|
+
budget.keys <= 500 &&
|
|
131
|
+
entries.every(([key, item]) => key.length >= 1 &&
|
|
132
|
+
key.length <= 100 &&
|
|
133
|
+
isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
134
|
+
}
|
|
135
|
+
export function hasBoundedManagedResourceBytes(value) {
|
|
136
|
+
return (new TextEncoder().encode(JSON.stringify(value)).byteLength <= 48 * 1_024);
|
|
137
|
+
}
|
|
138
|
+
export function normalizeManagedResourceDetail(value, attributeKeys) {
|
|
139
|
+
return {
|
|
140
|
+
id: value.id,
|
|
141
|
+
revision: value.revision,
|
|
142
|
+
status: value.status,
|
|
143
|
+
title: value.title,
|
|
144
|
+
...(value.description === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { description: value.description }),
|
|
147
|
+
...(value.attributes === undefined || attributeKeys.length === 0
|
|
148
|
+
? {}
|
|
149
|
+
: {
|
|
150
|
+
attributes: projectManagedResourceObject(value.attributes, attributeKeys),
|
|
151
|
+
}),
|
|
152
|
+
updatedAt: value.updatedAt,
|
|
153
|
+
value: value.value,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function normalizeManagedResourcePage(value, attributeKeys) {
|
|
157
|
+
return {
|
|
158
|
+
items: value.items.map((item) => ({
|
|
159
|
+
id: item.id,
|
|
160
|
+
revision: item.revision,
|
|
161
|
+
status: item.status,
|
|
162
|
+
title: item.title,
|
|
163
|
+
...(item.description === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { description: item.description }),
|
|
166
|
+
...(item.attributes === undefined || attributeKeys.length === 0
|
|
167
|
+
? {}
|
|
168
|
+
: {
|
|
169
|
+
attributes: projectManagedResourceObject(item.attributes, attributeKeys),
|
|
170
|
+
}),
|
|
171
|
+
updatedAt: item.updatedAt,
|
|
172
|
+
})),
|
|
173
|
+
...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
|
|
174
|
+
...(value.total === undefined ? {} : { total: value.total }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function normalizeManagedResourceRelationPage(value, valueKeys) {
|
|
178
|
+
return {
|
|
179
|
+
items: value.items.map((item) => ({
|
|
180
|
+
id: item.id,
|
|
181
|
+
value: projectManagedResourceObject(item.value, valueKeys),
|
|
182
|
+
})),
|
|
183
|
+
...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function projectManagedResourceObject(value, declaredKeys) {
|
|
187
|
+
const allowed = new Set(declaredKeys);
|
|
188
|
+
return Object.fromEntries(Object.entries(value)
|
|
189
|
+
.filter(([key]) => allowed.has(key))
|
|
190
|
+
.map(([key, item]) => [key, structuredClone(item)]));
|
|
191
|
+
}
|
|
192
|
+
export function managedResourceColumnKeys(module, resourceType) {
|
|
193
|
+
return (module.manifest.dashboard?.resources
|
|
194
|
+
?.find(({ id }) => id === resourceType)
|
|
195
|
+
?.columns?.map(({ key }) => key) ?? []);
|
|
196
|
+
}
|
|
197
|
+
function configurationValuesEqual(left, right) {
|
|
198
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
199
|
+
}
|
|
200
|
+
function isDefined(value) {
|
|
201
|
+
return value !== undefined;
|
|
202
|
+
}
|
|
203
|
+
function isRecord(value) {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=dashboard-feature-control.js.map
|
|
@@ -9,10 +9,15 @@ interface DashboardLogEventDefinition {
|
|
|
9
9
|
}
|
|
10
10
|
interface DashboardActionDefinition {
|
|
11
11
|
id: string;
|
|
12
|
+
permissionId?: string | undefined;
|
|
13
|
+
featureId?: string | undefined;
|
|
12
14
|
label: string;
|
|
13
15
|
description: string;
|
|
14
16
|
buttonLabel: string;
|
|
15
17
|
style: "primary" | "secondary" | "danger";
|
|
18
|
+
confirmation?: string | undefined;
|
|
19
|
+
inputs?: readonly unknown[] | undefined;
|
|
20
|
+
inspection?: "optional" | "required" | undefined;
|
|
16
21
|
}
|
|
17
22
|
type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
|
|
18
23
|
export interface DashboardModuleDefinition {
|
|
@@ -27,6 +32,10 @@ export interface DashboardModuleDefinition {
|
|
|
27
32
|
tier: ModuleTier;
|
|
28
33
|
requiredPermissions: string[];
|
|
29
34
|
settings: readonly unknown[];
|
|
35
|
+
settingsMutationPermissionId?: string;
|
|
36
|
+
settingsPreview?: Readonly<Record<string, unknown>>;
|
|
37
|
+
features: readonly unknown[];
|
|
38
|
+
permissionOperations: readonly unknown[];
|
|
30
39
|
commands: DashboardCommandDefinition[];
|
|
31
40
|
logEvents: DashboardLogEventDefinition[];
|
|
32
41
|
actions: DashboardActionDefinition[];
|
|
@@ -15,6 +15,21 @@ export function mapDashboardModule(module) {
|
|
|
15
15
|
tier: getModuleTier(module.manifest),
|
|
16
16
|
requiredPermissions: [...module.manifest.permissions],
|
|
17
17
|
settings: dashboard.settings,
|
|
18
|
+
...("settingsMutationPermissionId" in dashboard &&
|
|
19
|
+
dashboard.settingsMutationPermissionId
|
|
20
|
+
? {
|
|
21
|
+
settingsMutationPermissionId: dashboard.settingsMutationPermissionId,
|
|
22
|
+
}
|
|
23
|
+
: {}),
|
|
24
|
+
...("settingsPreview" in dashboard && dashboard.settingsPreview
|
|
25
|
+
? { settingsPreview: structuredClone(dashboard.settingsPreview) }
|
|
26
|
+
: {}),
|
|
27
|
+
features: module.manifest.manifestVersion === "0.5"
|
|
28
|
+
? module.manifest.features.map((feature) => structuredClone(feature))
|
|
29
|
+
: [],
|
|
30
|
+
permissionOperations: module.manifest.manifestVersion === "0.5"
|
|
31
|
+
? module.manifest.permissionOperations.map((operation) => structuredClone(operation))
|
|
32
|
+
: [],
|
|
18
33
|
commands: dashboardCommands(module),
|
|
19
34
|
logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
|
|
20
35
|
actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { BotControlHandler } from "./control-server.js";
|
|
3
|
+
/** Registers the bounded, non-persisting preview and import boundaries. */
|
|
4
|
+
export declare function registerDashboardPreviewImportRoutes(server: FastifyInstance, service: BotControlHandler): void;
|
|
5
|
+
export declare function registerDashboardResourceMutationRoutes(server: FastifyInstance, service: BotControlHandler): void;
|
|
6
|
+
//# sourceMappingURL=dashboard-preview-import-routes.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { hasBoundedManagedResourceBytes, isBoundedManagedResourceValue, } from "./dashboard-feature-control.js";
|
|
3
|
+
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
4
|
+
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
5
|
+
const resourceType = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
|
|
6
|
+
const mutationContext = z.object({
|
|
7
|
+
actorUserId: snowflake,
|
|
8
|
+
correlationId: z.uuid(),
|
|
9
|
+
});
|
|
10
|
+
const configurationValue = z.record(z.string(), z.union([
|
|
11
|
+
z.null(),
|
|
12
|
+
z.string(),
|
|
13
|
+
z.boolean(),
|
|
14
|
+
z.number().finite(),
|
|
15
|
+
z.array(z.string()).max(100),
|
|
16
|
+
]));
|
|
17
|
+
const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
|
|
18
|
+
const managedResourceObject = z
|
|
19
|
+
.record(z.string().min(1).max(100), managedResourceValue)
|
|
20
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
21
|
+
/** Registers the bounded, non-persisting preview and import boundaries. */
|
|
22
|
+
export function registerDashboardPreviewImportRoutes(server, service) {
|
|
23
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/configuration/preview", async (request) => {
|
|
24
|
+
const params = z
|
|
25
|
+
.object({ guildId: snowflake, moduleId })
|
|
26
|
+
.strict()
|
|
27
|
+
.parse(request.params);
|
|
28
|
+
const body = mutationContext
|
|
29
|
+
.extend({ value: configurationValue, input: managedResourceObject })
|
|
30
|
+
.strict()
|
|
31
|
+
.parse(request.body);
|
|
32
|
+
return service.previewConfiguration({ ...params, ...body });
|
|
33
|
+
});
|
|
34
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/import", async (request) => {
|
|
35
|
+
const params = z
|
|
36
|
+
.object({ guildId: snowflake, moduleId, resourceType })
|
|
37
|
+
.strict()
|
|
38
|
+
.parse(request.params);
|
|
39
|
+
const body = mutationContext
|
|
40
|
+
.extend({
|
|
41
|
+
format: z.literal("utf8_text"),
|
|
42
|
+
content: z
|
|
43
|
+
.string()
|
|
44
|
+
.min(1)
|
|
45
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= 48 * 1_024, "Managed resource import exceeds the transport byte limit"),
|
|
46
|
+
})
|
|
47
|
+
.strict()
|
|
48
|
+
.parse(request.body);
|
|
49
|
+
return service.importManagedResource({ ...params, ...body });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function registerDashboardResourceMutationRoutes(server, service) {
|
|
53
|
+
const resourceParams = z
|
|
54
|
+
.object({ guildId: snowflake, moduleId, resourceType })
|
|
55
|
+
.strict();
|
|
56
|
+
const identifiedResourceParams = resourceParams.extend({
|
|
57
|
+
resourceId: z.string().trim().min(1).max(100),
|
|
58
|
+
});
|
|
59
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/preview", async (request) => {
|
|
60
|
+
const params = resourceParams.parse(request.params);
|
|
61
|
+
const body = mutationContext
|
|
62
|
+
.extend({ value: managedResourceObject })
|
|
63
|
+
.strict()
|
|
64
|
+
.parse(request.body);
|
|
65
|
+
return service.previewManagedResource({ ...params, ...body });
|
|
66
|
+
});
|
|
67
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
|
|
68
|
+
const params = resourceParams.parse(request.params);
|
|
69
|
+
const body = mutationContext
|
|
70
|
+
.extend({
|
|
71
|
+
intent: z.enum(["draft", "publish"]).optional().default("publish"),
|
|
72
|
+
value: managedResourceObject,
|
|
73
|
+
})
|
|
74
|
+
.strict()
|
|
75
|
+
.parse(request.body);
|
|
76
|
+
return service.createManagedResource({ ...params, ...body });
|
|
77
|
+
});
|
|
78
|
+
server.put("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
|
|
79
|
+
const params = identifiedResourceParams.parse(request.params);
|
|
80
|
+
const body = mutationContext
|
|
81
|
+
.extend({
|
|
82
|
+
expectedRevision: z.number().int().min(0),
|
|
83
|
+
value: managedResourceObject.optional().default({}),
|
|
84
|
+
})
|
|
85
|
+
.strict()
|
|
86
|
+
.parse(request.body);
|
|
87
|
+
return service.updateManagedResource({ ...params, ...body });
|
|
88
|
+
});
|
|
89
|
+
server.delete("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
|
|
90
|
+
const params = identifiedResourceParams.parse(request.params);
|
|
91
|
+
const body = mutationContext
|
|
92
|
+
.extend({ expectedRevision: z.number().int().min(0) })
|
|
93
|
+
.strict()
|
|
94
|
+
.parse(request.body);
|
|
95
|
+
return service.deleteManagedResource({ ...params, ...body });
|
|
96
|
+
});
|
|
97
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId/actions/:actionId", async (request) => {
|
|
98
|
+
const params = identifiedResourceParams
|
|
99
|
+
.extend({ actionId: resourceType })
|
|
100
|
+
.parse(request.params);
|
|
101
|
+
const body = mutationContext
|
|
102
|
+
.extend({
|
|
103
|
+
expectedRevision: z.number().int().min(0),
|
|
104
|
+
value: managedResourceObject.optional().default({}),
|
|
105
|
+
actorAuthenticatedAt: z.iso.datetime({ offset: true }).optional(),
|
|
106
|
+
})
|
|
107
|
+
.strict()
|
|
108
|
+
.parse(request.body);
|
|
109
|
+
return service.executeManagedResourceAction({
|
|
110
|
+
...params,
|
|
111
|
+
actorUserId: body.actorUserId,
|
|
112
|
+
correlationId: body.correlationId,
|
|
113
|
+
expectedRevision: body.expectedRevision,
|
|
114
|
+
value: body.value,
|
|
115
|
+
...(body.actorAuthenticatedAt === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { actorAuthenticatedAt: body.actorAuthenticatedAt }),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=dashboard-preview-import-routes.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DiscordAnalyticsInspectionService } from "@helyx/sdk";
|
|
2
2
|
import { type Client } from "discord.js";
|
|
3
3
|
export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspectionService {
|
|
4
|
+
#private;
|
|
4
5
|
private readonly client;
|
|
5
6
|
constructor(client: Client);
|
|
6
7
|
listChannels(input: {
|
|
@@ -25,6 +26,8 @@ export declare class DiscordAnalyticsInspection implements DiscordAnalyticsInspe
|
|
|
25
26
|
limit: number;
|
|
26
27
|
includeArchived: boolean;
|
|
27
28
|
includePrivate: boolean;
|
|
29
|
+
signal?: AbortSignal;
|
|
28
30
|
}): ReturnType<DiscordAnalyticsInspectionService["listThreads"]>;
|
|
31
|
+
private activeThreads;
|
|
29
32
|
}
|
|
30
33
|
//# sourceMappingURL=discord-analytics.d.ts.map
|
|
@@ -2,8 +2,10 @@ import { ChannelType, GatewayIntentBits, PermissionFlagsBits, } from "discord.js
|
|
|
2
2
|
const MAX_MESSAGE_PAGE = 100;
|
|
3
3
|
const MAX_MEMBER_PAGE = 1_000;
|
|
4
4
|
const MAX_THREAD_PAGE = 100;
|
|
5
|
+
const ACTIVE_THREAD_CACHE_MS = 15_000;
|
|
5
6
|
export class DiscordAnalyticsInspection {
|
|
6
7
|
client;
|
|
8
|
+
#activeThreads = new WeakMap();
|
|
7
9
|
constructor(client) {
|
|
8
10
|
this.client = client;
|
|
9
11
|
}
|
|
@@ -147,12 +149,12 @@ export class DiscordAnalyticsInspection {
|
|
|
147
149
|
if (input.cursor && !cursor)
|
|
148
150
|
return unavailable("not_supported");
|
|
149
151
|
try {
|
|
150
|
-
const parents = await threadParents(guild, input.parentChannelId);
|
|
152
|
+
const parents = await abortable(threadParents(guild, input.parentChannelId), input.signal);
|
|
151
153
|
const privateCoverage = privateThreadCoverage(guild);
|
|
152
154
|
const current = cursor ?? { phase: "active", offset: 0 };
|
|
153
155
|
if (current.phase === "active") {
|
|
154
|
-
const active = await
|
|
155
|
-
const facts =
|
|
156
|
+
const active = await abortable(this.activeThreads(guild), input.signal);
|
|
157
|
+
const facts = active
|
|
156
158
|
.filter((thread) => !input.parentChannelId ||
|
|
157
159
|
thread.parentId === input.parentChannelId)
|
|
158
160
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
@@ -183,12 +185,12 @@ export class DiscordAnalyticsInspection {
|
|
|
183
185
|
const phase = phases[phaseIndex];
|
|
184
186
|
while (parentIndex < parents.length) {
|
|
185
187
|
const parent = parents[parentIndex];
|
|
186
|
-
const page = await parent.threads.fetchArchived({
|
|
188
|
+
const page = await abortable(parent.threads.fetchArchived({
|
|
187
189
|
type: phase,
|
|
188
190
|
fetchAll: phase === "private" && privateCoverage === "all",
|
|
189
191
|
limit,
|
|
190
192
|
...(before ? { before } : {}),
|
|
191
|
-
}, false);
|
|
193
|
+
}, false), input.signal);
|
|
192
194
|
const threads = [...page.threads.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
193
195
|
const items = threads.map((thread) => threadFact(guild, thread));
|
|
194
196
|
if (page.hasMore && threads.length > 0) {
|
|
@@ -228,6 +230,23 @@ export class DiscordAnalyticsInspection {
|
|
|
228
230
|
return unavailable("discord_unavailable");
|
|
229
231
|
}
|
|
230
232
|
}
|
|
233
|
+
activeThreads(guild) {
|
|
234
|
+
const cached = this.#activeThreads.get(guild);
|
|
235
|
+
if (cached && cached.expiresAt > Date.now())
|
|
236
|
+
return cached.promise;
|
|
237
|
+
const promise = guild.channels
|
|
238
|
+
.fetchActiveThreads(false)
|
|
239
|
+
.then((response) => [...response.threads.values()]);
|
|
240
|
+
this.#activeThreads.set(guild, {
|
|
241
|
+
expiresAt: Date.now() + ACTIVE_THREAD_CACHE_MS,
|
|
242
|
+
promise,
|
|
243
|
+
});
|
|
244
|
+
void promise.catch(() => {
|
|
245
|
+
if (this.#activeThreads.get(guild)?.promise === promise)
|
|
246
|
+
this.#activeThreads.delete(guild);
|
|
247
|
+
});
|
|
248
|
+
return promise;
|
|
249
|
+
}
|
|
231
250
|
}
|
|
232
251
|
function analyticsChannelType(channel) {
|
|
233
252
|
switch (channel.type) {
|
|
@@ -246,6 +265,10 @@ function analyticsChannelType(channel) {
|
|
|
246
265
|
}
|
|
247
266
|
}
|
|
248
267
|
async function threadParents(guild, parentChannelId) {
|
|
268
|
+
if (parentChannelId) {
|
|
269
|
+
const channel = await guild.channels.fetch(parentChannelId);
|
|
270
|
+
return channel && "threads" in channel ? [channel] : [];
|
|
271
|
+
}
|
|
249
272
|
const channels = await guild.channels.fetch();
|
|
250
273
|
return [...channels.values()]
|
|
251
274
|
.filter((channel) => channel !== null &&
|
|
@@ -334,4 +357,25 @@ function snowflakeTimestamp(id) {
|
|
|
334
357
|
function unavailable(code) {
|
|
335
358
|
return { available: false, code };
|
|
336
359
|
}
|
|
360
|
+
function abortable(promise, signal) {
|
|
361
|
+
if (!signal)
|
|
362
|
+
return promise;
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
const abort = () => reject(signal.reason instanceof Error
|
|
365
|
+
? signal.reason
|
|
366
|
+
: new Error("Discord Analytics inspection was aborted"));
|
|
367
|
+
if (signal.aborted) {
|
|
368
|
+
abort();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
372
|
+
void promise.then((value) => {
|
|
373
|
+
signal.removeEventListener("abort", abort);
|
|
374
|
+
resolve(value);
|
|
375
|
+
}, (error) => {
|
|
376
|
+
signal.removeEventListener("abort", abort);
|
|
377
|
+
reject(error instanceof Error ? error : new Error("Discord read failed"));
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
337
381
|
//# sourceMappingURL=discord-analytics.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DiscordAutoModerationAction, DiscordAutoModerationMutationResult, DiscordAutoModerationRuleSnapshot, DiscordAutoModerationService } from "@helyx/sdk";
|
|
2
|
+
import { type AutoModerationAction, type AutoModerationRule, type Client } from "discord.js";
|
|
3
|
+
/** Discord REST adapter scoped to guilds owned by this client/shard. */
|
|
4
|
+
export declare class DiscordAutoModerationAdapter implements DiscordAutoModerationService {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly client;
|
|
7
|
+
constructor(client: Client);
|
|
8
|
+
list(input: {
|
|
9
|
+
guildId: string;
|
|
10
|
+
}): Promise<readonly DiscordAutoModerationRuleSnapshot[]>;
|
|
11
|
+
read(input: {
|
|
12
|
+
guildId: string;
|
|
13
|
+
ruleId: string;
|
|
14
|
+
}): Promise<DiscordAutoModerationRuleSnapshot | null>;
|
|
15
|
+
create(input: Parameters<DiscordAutoModerationService["create"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
16
|
+
update(input: Parameters<DiscordAutoModerationService["update"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
17
|
+
delete(input: Parameters<DiscordAutoModerationService["delete"]>[0]): Promise<DiscordAutoModerationMutationResult>;
|
|
18
|
+
}
|
|
19
|
+
export declare function toRuleSnapshot(rule: AutoModerationRule): DiscordAutoModerationRuleSnapshot;
|
|
20
|
+
export declare function fromDiscordAction(action: AutoModerationAction): DiscordAutoModerationAction;
|
|
21
|
+
//# sourceMappingURL=discord-auto-moderation-service.d.ts.map
|