@helyx/bot 0.4.2 → 0.4.4
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 +29 -0
- package/README.md +13 -0
- package/dist/cloud-connector.d.ts +7 -0
- package/dist/cloud-connector.js +118 -1
- package/dist/cloud-control-dispatcher.js +84 -0
- package/dist/control-server.d.ts +187 -5
- package/dist/control-server.js +673 -6
- package/dist/developer-operations-module.js +8 -0
- package/dist/discord-owned-message-service.d.ts +39 -0
- package/dist/discord-owned-message-service.js +138 -0
- package/dist/events.d.ts +70 -1
- package/dist/events.js +557 -5
- package/dist/index.js +61 -13
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +66 -31
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +19 -1
- package/dist/modules.d.ts +9 -0
- package/dist/modules.js +45 -0
- package/dist/resource-authorization.d.ts +7 -0
- package/dist/resource-authorization.js +12 -0
- package/dist/scheduled-task-runner.d.ts +130 -0
- package/dist/scheduled-task-runner.js +417 -0
- package/dist/shard-cloud-proxy.d.ts +4 -0
- package/dist/shard-cloud-proxy.js +34 -0
- package/dist/shard-contracts.d.ts +574 -23
- package/dist/shard-contracts.js +346 -0
- package/dist/shard-coordinator.d.ts +5 -3
- package/dist/shard-coordinator.js +85 -2
- package/dist/shard-router.d.ts +10 -0
- package/dist/shard-router.js +58 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +187 -5
- package/dist/shard-worker.d.ts +9 -3
- package/dist/shard-worker.js +97 -16
- package/dist/support-module.d.ts +9 -0
- package/dist/support-module.js +172 -0
- package/package.json +9 -9
package/dist/control-server.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AuditRepository, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
|
|
2
2
|
import { DashboardActionValidationError, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
|
|
3
|
-
import {
|
|
3
|
+
import { canUseCommand } from "@helyx/core";
|
|
4
|
+
import { ChannelType, PermissionFlagsBits } from "discord.js";
|
|
4
5
|
import Fastify, {} from "fastify";
|
|
5
6
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
6
7
|
import { z, ZodError } from "zod";
|
|
7
|
-
import { deleteChannelMessage as deleteDiscordChannelMessage, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
|
|
8
|
+
import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
|
|
8
9
|
import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
|
|
9
10
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
10
11
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
@@ -41,6 +42,55 @@ const mutationContext = z.object({
|
|
|
41
42
|
actorUserId: snowflake,
|
|
42
43
|
correlationId: z.uuid(),
|
|
43
44
|
});
|
|
45
|
+
const resourceId = z.string().trim().min(1).max(100);
|
|
46
|
+
const resourceCursor = z.string().min(1).max(2_048);
|
|
47
|
+
const resourceActionId = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
|
|
48
|
+
const managedResourceFilters = z
|
|
49
|
+
.record(z.string().regex(/^[a-z][a-zA-Z0-9_]*$/u), z.string().trim().min(1).max(100))
|
|
50
|
+
.refine((value) => Object.keys(value).length <= 5);
|
|
51
|
+
const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
|
|
52
|
+
const managedResourceObject = z
|
|
53
|
+
.record(z.string().min(1).max(100), managedResourceValue)
|
|
54
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
55
|
+
const managedResourceSummarySchema = z
|
|
56
|
+
.object({
|
|
57
|
+
id: resourceId,
|
|
58
|
+
revision: z.number().int().min(0),
|
|
59
|
+
status: z.string().trim().min(1).max(100),
|
|
60
|
+
title: z.string().trim().min(1).max(200),
|
|
61
|
+
description: z.string().max(500).optional(),
|
|
62
|
+
attributes: managedResourceObject.optional(),
|
|
63
|
+
updatedAt: z.iso.datetime({ offset: true }),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const managedResourceDetailSchema = managedResourceSummarySchema
|
|
67
|
+
.extend({ value: managedResourceObject })
|
|
68
|
+
.strict()
|
|
69
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
70
|
+
const managedResourcePageSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
items: z.array(managedResourceSummarySchema).max(50),
|
|
73
|
+
nextCursor: resourceCursor.optional(),
|
|
74
|
+
})
|
|
75
|
+
.strict()
|
|
76
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
77
|
+
const managedResourceRelationPageSchema = z
|
|
78
|
+
.object({
|
|
79
|
+
items: z
|
|
80
|
+
.array(z
|
|
81
|
+
.object({
|
|
82
|
+
id: resourceId,
|
|
83
|
+
value: managedResourceObject,
|
|
84
|
+
})
|
|
85
|
+
.strict())
|
|
86
|
+
.max(50),
|
|
87
|
+
nextCursor: resourceCursor.optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict()
|
|
90
|
+
.refine(hasBoundedManagedResourceBytes);
|
|
91
|
+
const managedResourceDeleteResultSchema = z
|
|
92
|
+
.object({ deleted: z.boolean() })
|
|
93
|
+
.strict();
|
|
44
94
|
export class DiscordGuildResourceDirectory {
|
|
45
95
|
client;
|
|
46
96
|
constructor(client) {
|
|
@@ -87,6 +137,55 @@ export class DiscordGuildResourceDirectory {
|
|
|
87
137
|
deleteChannelMessage(channelId, messageId) {
|
|
88
138
|
return deleteDiscordChannelMessage(this.client, channelId, messageId);
|
|
89
139
|
}
|
|
140
|
+
editChannelMessage(channelId, messageId, response) {
|
|
141
|
+
return editDiscordChannelMessage(this.client, channelId, messageId, response);
|
|
142
|
+
}
|
|
143
|
+
addChannelMessageReaction(channelId, messageId, emojiIdentifier) {
|
|
144
|
+
return addDiscordChannelMessageReaction(this.client, channelId, messageId, emojiIdentifier);
|
|
145
|
+
}
|
|
146
|
+
async resolveActor(guildId, actorUserId) {
|
|
147
|
+
try {
|
|
148
|
+
const guild = await this.client.guilds.fetch(guildId);
|
|
149
|
+
const member = await guild.members.fetch(actorUserId);
|
|
150
|
+
return {
|
|
151
|
+
userId: member.id,
|
|
152
|
+
roleIds: [...member.roles.cache.keys()],
|
|
153
|
+
permissions: member.permissions
|
|
154
|
+
.toArray()
|
|
155
|
+
.map(normalizeDiscordPermission),
|
|
156
|
+
isServerOwner: guild.ownerId === member.id,
|
|
157
|
+
isAdministrator: member.permissions.has(PermissionFlagsBits.Administrator),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (isUnknownGuildError(error))
|
|
162
|
+
throw new BotControlNotFoundError();
|
|
163
|
+
if (isUnknownMemberError(error))
|
|
164
|
+
throw new BotControlForbiddenError();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async sendPrivateSupportNotification(guildId, channelId, response, notificationId) {
|
|
169
|
+
let guild;
|
|
170
|
+
try {
|
|
171
|
+
guild = await this.client.guilds.fetch(guildId);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (isUnknownGuildError(error))
|
|
175
|
+
throw new BotControlNotFoundError();
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const channel = await guild.channels.fetch(channelId);
|
|
179
|
+
if (!channel ||
|
|
180
|
+
(channel.type !== ChannelType.GuildText &&
|
|
181
|
+
channel.type !== ChannelType.GuildAnnouncement) ||
|
|
182
|
+
!channel.isSendable() ||
|
|
183
|
+
channel
|
|
184
|
+
.permissionsFor(guild.roles.everyone)
|
|
185
|
+
?.has(PermissionFlagsBits.ViewChannel))
|
|
186
|
+
throw new BotControlNotFoundError("The support notification channel is missing or not private");
|
|
187
|
+
await sendDiscordMessageToChannel(this.client, channel.id, response, notificationId);
|
|
188
|
+
}
|
|
90
189
|
}
|
|
91
190
|
export class BotControlTransportError extends Error {
|
|
92
191
|
status;
|
|
@@ -99,6 +198,7 @@ export class BotControlTransportError extends Error {
|
|
|
99
198
|
export class BotControlService {
|
|
100
199
|
database;
|
|
101
200
|
resources;
|
|
201
|
+
supportNotifications;
|
|
102
202
|
#modules;
|
|
103
203
|
#dashboardModules;
|
|
104
204
|
#configurations;
|
|
@@ -106,9 +206,10 @@ export class BotControlService {
|
|
|
106
206
|
#permissions;
|
|
107
207
|
#services;
|
|
108
208
|
#privacyProviders;
|
|
109
|
-
constructor(database, modules, resources, services, privacyExecutor) {
|
|
209
|
+
constructor(database, modules, resources, services, privacyExecutor, supportNotifications) {
|
|
110
210
|
this.database = database;
|
|
111
211
|
this.resources = resources;
|
|
212
|
+
this.supportNotifications = supportNotifications;
|
|
112
213
|
this.#modules = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
113
214
|
this.#dashboardModules = modules
|
|
114
215
|
.filter((module) => module.manifest.dashboard !== undefined)
|
|
@@ -126,6 +227,40 @@ export class BotControlService {
|
|
|
126
227
|
})
|
|
127
228
|
: []);
|
|
128
229
|
}
|
|
230
|
+
async sendSupportNotification(input) {
|
|
231
|
+
if (!this.supportNotifications)
|
|
232
|
+
throw new BotControlNotFoundError("The support notification channel is not configured");
|
|
233
|
+
if (!this.resources.sendPrivateSupportNotification)
|
|
234
|
+
throw new BotControlTransportError(503, "Support notification delivery is temporarily unavailable");
|
|
235
|
+
const caseType = {
|
|
236
|
+
bug: "Bug",
|
|
237
|
+
general_support: "General support",
|
|
238
|
+
security: "Security report",
|
|
239
|
+
}[input.caseType];
|
|
240
|
+
const source = input.source === "discord_command" ? "/bug" : "Dashboard";
|
|
241
|
+
const severity = input.reportedSeverity
|
|
242
|
+
? `${input.reportedSeverity[0].toUpperCase()}${input.reportedSeverity.slice(1)}`
|
|
243
|
+
: "Untriaged";
|
|
244
|
+
await this.resources.sendPrivateSupportNotification(this.supportNotifications.guildId, this.supportNotifications.channelId, {
|
|
245
|
+
embed: {
|
|
246
|
+
title: input.caseType === "security" ||
|
|
247
|
+
input.reportedSeverity === "critical"
|
|
248
|
+
? "Urgent support case received"
|
|
249
|
+
: "New support case",
|
|
250
|
+
description: [
|
|
251
|
+
`**${input.reference}**`,
|
|
252
|
+
`Case type: ${caseType}`,
|
|
253
|
+
`Category or module: ${input.categoryOrModule}`,
|
|
254
|
+
`Severity: ${severity}`,
|
|
255
|
+
`Source: ${source}`,
|
|
256
|
+
].join("\n"),
|
|
257
|
+
color: input.caseType === "security" ||
|
|
258
|
+
input.reportedSeverity === "critical"
|
|
259
|
+
? 0xdc2626
|
|
260
|
+
: 0x7c3aed,
|
|
261
|
+
},
|
|
262
|
+
}, input.notificationId);
|
|
263
|
+
}
|
|
129
264
|
listPrivacyProviders() {
|
|
130
265
|
return Promise.resolve([...this.#privacyProviders.values()]
|
|
131
266
|
.map((provider) => ({
|
|
@@ -391,6 +526,7 @@ export class BotControlService {
|
|
|
391
526
|
if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, input.moduleId))) {
|
|
392
527
|
throw new BotControlValidationError("Enable this module before running its dashboard actions");
|
|
393
528
|
}
|
|
529
|
+
await this.#authorizeAction(input.guildId, module, input.actorUserId, dashboardActionPermissionId(module, input.actionId));
|
|
394
530
|
return action.execute({
|
|
395
531
|
guildId: input.guildId,
|
|
396
532
|
actorUserId: input.actorUserId,
|
|
@@ -400,6 +536,151 @@ export class BotControlService {
|
|
|
400
536
|
deleteChannelMessage: (channelId, messageId) => this.resources.deleteChannelMessage(channelId, messageId),
|
|
401
537
|
});
|
|
402
538
|
}
|
|
539
|
+
async listManagedResources(input) {
|
|
540
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
541
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "list");
|
|
542
|
+
this.#validateManagedResourceQuery(module, input.resourceType, input);
|
|
543
|
+
const result = await resource.list(await this.#managedResourceContext(input, module, "list"), {
|
|
544
|
+
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
545
|
+
limit: input.limit,
|
|
546
|
+
...(input.search === undefined ? {} : { search: input.search }),
|
|
547
|
+
...(input.filters === undefined ? {} : { filters: input.filters }),
|
|
548
|
+
});
|
|
549
|
+
return normalizeManagedResourcePage(managedResourcePageSchema.parse(result), managedResourceColumnKeys(module, input.resourceType));
|
|
550
|
+
}
|
|
551
|
+
async readManagedResource(input) {
|
|
552
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
553
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "read");
|
|
554
|
+
const result = await resource.read(await this.#managedResourceContext(input, module, "read"), input.resourceId);
|
|
555
|
+
if (!result)
|
|
556
|
+
throw new BotControlNotFoundError("Managed resource was not found");
|
|
557
|
+
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(result), managedResourceColumnKeys(module, input.resourceType));
|
|
558
|
+
}
|
|
559
|
+
async listManagedResourceRelation(input) {
|
|
560
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
561
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "read");
|
|
562
|
+
const relation = module.manifest.dashboard?.resources
|
|
563
|
+
?.find(({ id }) => id === input.resourceType)
|
|
564
|
+
?.relations?.find(({ id }) => id === input.relationId);
|
|
565
|
+
if (!relation || !resource.listRelation)
|
|
566
|
+
throw new BotControlNotFoundError("Managed resource relation is not declared by this module");
|
|
567
|
+
const context = await this.#managedResourceContext(input, module, "read", relation.permissionId);
|
|
568
|
+
return normalizeManagedResourceRelationPage(managedResourceRelationPageSchema.parse(await resource.listRelation(context, input.resourceId, input.relationId, {
|
|
569
|
+
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
570
|
+
limit: input.limit,
|
|
571
|
+
})), relation.columns.map(({ key }) => key));
|
|
572
|
+
}
|
|
573
|
+
async previewManagedResource(input) {
|
|
574
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
575
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "preview");
|
|
576
|
+
return resource.preview(await this.#managedResourceContext(input, module, "preview"), input.value);
|
|
577
|
+
}
|
|
578
|
+
async createManagedResource(input) {
|
|
579
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
580
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "create");
|
|
581
|
+
const context = await this.#managedResourceContext(input, module, "create");
|
|
582
|
+
const declaredIntents = module.manifest.dashboard?.resources?.find(({ id }) => id === input.resourceType)?.creationIntents ?? ["publish"];
|
|
583
|
+
if (!declaredIntents.includes(input.intent))
|
|
584
|
+
throw new DashboardActionValidationError("This creation intent is not declared by the module.");
|
|
585
|
+
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.create(context, input.value, input.intent)), managedResourceColumnKeys(module, input.resourceType));
|
|
586
|
+
}
|
|
587
|
+
async updateManagedResource(input) {
|
|
588
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
589
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "update");
|
|
590
|
+
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.update(await this.#managedResourceContext(input, module, "update"), {
|
|
591
|
+
resourceId: input.resourceId,
|
|
592
|
+
expectedRevision: input.expectedRevision,
|
|
593
|
+
value: input.value,
|
|
594
|
+
})), managedResourceColumnKeys(module, input.resourceType));
|
|
595
|
+
}
|
|
596
|
+
async deleteManagedResource(input) {
|
|
597
|
+
const { module, resource } = await this.#prepareManagedResource(input);
|
|
598
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "delete");
|
|
599
|
+
return managedResourceDeleteResultSchema.parse(await resource.delete(await this.#managedResourceContext(input, module, "delete"), {
|
|
600
|
+
resourceId: input.resourceId,
|
|
601
|
+
expectedRevision: input.expectedRevision,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
async executeManagedResourceAction(input) {
|
|
605
|
+
const module = this.#requireDashboardModule(input.moduleId);
|
|
606
|
+
const declared = module.manifest.dashboard?.resources
|
|
607
|
+
?.find(({ id }) => id === input.resourceType)
|
|
608
|
+
?.actions?.find(({ id }) => id === input.actionId);
|
|
609
|
+
const resource = this.#requireManagedResource(module, input.resourceType);
|
|
610
|
+
this.#requireManagedResourceOperation(module, input.resourceType, "action");
|
|
611
|
+
if (!declared)
|
|
612
|
+
throw new BotControlNotFoundError("Managed resource action is not declared by this module");
|
|
613
|
+
await this.#requireEnabledManagedResource(input.guildId, input.moduleId);
|
|
614
|
+
const permissionId = managedResourcePermissionId(module, input.resourceType, "action", input.actionId, declared.permissionId);
|
|
615
|
+
const context = await this.#managedResourceContext(input, module, "action", permissionId);
|
|
616
|
+
validateManagedResourceActionValue(declared.inputs ?? [], input.value);
|
|
617
|
+
const result = await resource.executeAction(context, {
|
|
618
|
+
resourceId: input.resourceId,
|
|
619
|
+
actionId: input.actionId,
|
|
620
|
+
expectedRevision: input.expectedRevision,
|
|
621
|
+
value: input.value,
|
|
622
|
+
});
|
|
623
|
+
return result === null
|
|
624
|
+
? null
|
|
625
|
+
: normalizeManagedResourceDetail(managedResourceDetailSchema.parse(result), managedResourceColumnKeys(module, input.resourceType));
|
|
626
|
+
}
|
|
627
|
+
async #prepareManagedResource(input) {
|
|
628
|
+
const module = this.#requireDashboardModule(input.moduleId);
|
|
629
|
+
const resource = this.#requireManagedResource(module, input.resourceType);
|
|
630
|
+
await this.#requireEnabledManagedResource(input.guildId, input.moduleId);
|
|
631
|
+
return { module, resource };
|
|
632
|
+
}
|
|
633
|
+
#requireManagedResource(module, resourceType) {
|
|
634
|
+
const declared = module.manifest.dashboard?.resources?.some(({ id }) => id === resourceType);
|
|
635
|
+
const resource = module.managedResources?.find(({ id }) => id === resourceType);
|
|
636
|
+
if (!declared || !resource)
|
|
637
|
+
throw new BotControlNotFoundError("Managed resource is not declared by this module");
|
|
638
|
+
return resource;
|
|
639
|
+
}
|
|
640
|
+
#requireManagedResourceOperation(module, resourceType, operation) {
|
|
641
|
+
const declared = module.manifest.dashboard?.resources?.find(({ id }) => id === resourceType);
|
|
642
|
+
if (!declared ||
|
|
643
|
+
(declared.operations !== undefined &&
|
|
644
|
+
!declared.operations.includes(operation)))
|
|
645
|
+
throw new BotControlNotFoundError("Managed resource operation is not declared by this module");
|
|
646
|
+
}
|
|
647
|
+
#validateManagedResourceQuery(module, resourceType, input) {
|
|
648
|
+
const query = module.manifest.dashboard?.resources?.find(({ id }) => id === resourceType)?.query;
|
|
649
|
+
if (input.search !== undefined && !query?.search)
|
|
650
|
+
throw new BotControlValidationError("Managed resource search is not declared by this module");
|
|
651
|
+
for (const [key, value] of Object.entries(input.filters ?? {})) {
|
|
652
|
+
const filter = query?.filters?.find((candidate) => candidate.key === key);
|
|
653
|
+
if (!filter)
|
|
654
|
+
throw new BotControlValidationError("Managed resource filter is not declared by this module");
|
|
655
|
+
if (filter.type === "select" &&
|
|
656
|
+
!filter.options.some((option) => option.value === value))
|
|
657
|
+
throw new BotControlValidationError("Managed resource filter value is not declared by this module");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async #requireEnabledManagedResource(guildId, moduleIdValue) {
|
|
661
|
+
if (!(await new InstallationRepository(this.database).isModuleEnabled(guildId, moduleIdValue)))
|
|
662
|
+
throw new BotControlValidationError("Enable this module before managing its resources");
|
|
663
|
+
}
|
|
664
|
+
async #managedResourceContext(input, module, operation, permissionId = managedResourcePermissionId(module, input.resourceType, operation)) {
|
|
665
|
+
const actor = await this.#authorizeAction(input.guildId, module, input.actorUserId, permissionId);
|
|
666
|
+
return {
|
|
667
|
+
guildId: input.guildId,
|
|
668
|
+
actor,
|
|
669
|
+
correlationId: input.correlationId,
|
|
670
|
+
services: this.#services,
|
|
671
|
+
sendToChannel: (channelId, response) => this.resources.sendToChannel(channelId, response),
|
|
672
|
+
deleteChannelMessage: (channelId, messageId) => this.resources.deleteChannelMessage(channelId, messageId),
|
|
673
|
+
editChannelMessage: (channelId, messageId, response) => this.resources.editChannelMessage(channelId, messageId, response),
|
|
674
|
+
addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => this.resources.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async #authorizeAction(guildId, module, actorUserId, permissionId) {
|
|
678
|
+
const actor = await this.resources.resolveActor(guildId, actorUserId);
|
|
679
|
+
const policy = await this.#permissions.getEffectivePolicy(guildId, module.manifest.id, permissionId);
|
|
680
|
+
if (!canUseCommand(actor, policy))
|
|
681
|
+
throw new BotControlForbiddenError();
|
|
682
|
+
return actor;
|
|
683
|
+
}
|
|
403
684
|
#requireModule(moduleIdValue) {
|
|
404
685
|
const module = this.#modules.get(moduleIdValue);
|
|
405
686
|
if (!module)
|
|
@@ -570,6 +851,28 @@ export async function createBotControlServer(input) {
|
|
|
570
851
|
};
|
|
571
852
|
});
|
|
572
853
|
server.get("/v1/privacy/providers", () => input.service.listPrivacyProviders());
|
|
854
|
+
server.post("/v1/support/notifications", async (request, reply) => {
|
|
855
|
+
const body = z
|
|
856
|
+
.object({
|
|
857
|
+
notificationId: z.uuid(),
|
|
858
|
+
reference: z.string().trim().min(1).max(40),
|
|
859
|
+
caseType: z.enum(["bug", "general_support", "security"]),
|
|
860
|
+
categoryOrModule: z.string().trim().min(1).max(100),
|
|
861
|
+
reportedSeverity: z
|
|
862
|
+
.enum(["low", "medium", "high", "critical"])
|
|
863
|
+
.nullable(),
|
|
864
|
+
source: z.enum([
|
|
865
|
+
"dashboard_bug",
|
|
866
|
+
"dashboard_support",
|
|
867
|
+
"dashboard_security",
|
|
868
|
+
"discord_command",
|
|
869
|
+
]),
|
|
870
|
+
})
|
|
871
|
+
.strict()
|
|
872
|
+
.parse(request.body);
|
|
873
|
+
await input.service.sendSupportNotification(body);
|
|
874
|
+
return reply.code(204).send();
|
|
875
|
+
});
|
|
573
876
|
server.post("/v1/privacy/capture", async (request) => {
|
|
574
877
|
const body = z
|
|
575
878
|
.object({
|
|
@@ -630,6 +933,181 @@ export async function createBotControlServer(input) {
|
|
|
630
933
|
const body = mutationContext.parse(request.body);
|
|
631
934
|
return input.service.executeDashboardAction({ ...params, ...body });
|
|
632
935
|
});
|
|
936
|
+
server.get("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
|
|
937
|
+
const params = z
|
|
938
|
+
.object({
|
|
939
|
+
guildId: snowflake,
|
|
940
|
+
moduleId,
|
|
941
|
+
resourceType: resourceActionId,
|
|
942
|
+
})
|
|
943
|
+
.strict()
|
|
944
|
+
.parse(request.params);
|
|
945
|
+
const query = z
|
|
946
|
+
.object({
|
|
947
|
+
cursor: resourceCursor.optional(),
|
|
948
|
+
limit: z.coerce.number().int().min(1).max(50).default(50),
|
|
949
|
+
search: z.string().trim().min(1).max(200).optional(),
|
|
950
|
+
filters: z
|
|
951
|
+
.preprocess((value) => {
|
|
952
|
+
if (typeof value !== "string")
|
|
953
|
+
return value;
|
|
954
|
+
if (value.length > 2_048)
|
|
955
|
+
return value;
|
|
956
|
+
try {
|
|
957
|
+
return JSON.parse(value);
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
}, managedResourceFilters)
|
|
963
|
+
.optional(),
|
|
964
|
+
actorUserId: snowflake,
|
|
965
|
+
correlationId: z.uuid(),
|
|
966
|
+
})
|
|
967
|
+
.strict()
|
|
968
|
+
.parse(request.query);
|
|
969
|
+
return input.service.listManagedResources({
|
|
970
|
+
...params,
|
|
971
|
+
...(query.cursor === undefined ? {} : { cursor: query.cursor }),
|
|
972
|
+
...(query.search === undefined ? {} : { search: query.search }),
|
|
973
|
+
...(query.filters === undefined ? {} : { filters: query.filters }),
|
|
974
|
+
limit: query.limit,
|
|
975
|
+
actorUserId: query.actorUserId,
|
|
976
|
+
correlationId: query.correlationId,
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
server.get("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
|
|
980
|
+
const params = z
|
|
981
|
+
.object({
|
|
982
|
+
guildId: snowflake,
|
|
983
|
+
moduleId,
|
|
984
|
+
resourceType: resourceActionId,
|
|
985
|
+
resourceId,
|
|
986
|
+
})
|
|
987
|
+
.strict()
|
|
988
|
+
.parse(request.params);
|
|
989
|
+
const query = mutationContext.strict().parse(request.query);
|
|
990
|
+
return input.service.readManagedResource({ ...params, ...query });
|
|
991
|
+
});
|
|
992
|
+
server.get("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId/relations/:relationId", async (request) => {
|
|
993
|
+
const params = z
|
|
994
|
+
.object({
|
|
995
|
+
guildId: snowflake,
|
|
996
|
+
moduleId,
|
|
997
|
+
resourceType: resourceActionId,
|
|
998
|
+
resourceId,
|
|
999
|
+
relationId: resourceActionId,
|
|
1000
|
+
})
|
|
1001
|
+
.strict()
|
|
1002
|
+
.parse(request.params);
|
|
1003
|
+
const query = z
|
|
1004
|
+
.object({
|
|
1005
|
+
cursor: resourceCursor.optional(),
|
|
1006
|
+
limit: z.coerce.number().int().min(1).max(50).default(50),
|
|
1007
|
+
actorUserId: snowflake,
|
|
1008
|
+
correlationId: z.uuid(),
|
|
1009
|
+
})
|
|
1010
|
+
.strict()
|
|
1011
|
+
.parse(request.query);
|
|
1012
|
+
return input.service.listManagedResourceRelation({
|
|
1013
|
+
...params,
|
|
1014
|
+
...(query.cursor === undefined ? {} : { cursor: query.cursor }),
|
|
1015
|
+
limit: query.limit,
|
|
1016
|
+
actorUserId: query.actorUserId,
|
|
1017
|
+
correlationId: query.correlationId,
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/preview", async (request) => {
|
|
1021
|
+
const params = z
|
|
1022
|
+
.object({
|
|
1023
|
+
guildId: snowflake,
|
|
1024
|
+
moduleId,
|
|
1025
|
+
resourceType: resourceActionId,
|
|
1026
|
+
})
|
|
1027
|
+
.strict()
|
|
1028
|
+
.parse(request.params);
|
|
1029
|
+
const body = mutationContext
|
|
1030
|
+
.extend({ value: managedResourceObject })
|
|
1031
|
+
.strict()
|
|
1032
|
+
.parse(request.body);
|
|
1033
|
+
return input.service.previewManagedResource({ ...params, ...body });
|
|
1034
|
+
});
|
|
1035
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
|
|
1036
|
+
const params = z
|
|
1037
|
+
.object({
|
|
1038
|
+
guildId: snowflake,
|
|
1039
|
+
moduleId,
|
|
1040
|
+
resourceType: resourceActionId,
|
|
1041
|
+
})
|
|
1042
|
+
.strict()
|
|
1043
|
+
.parse(request.params);
|
|
1044
|
+
const body = mutationContext
|
|
1045
|
+
.extend({
|
|
1046
|
+
intent: z.enum(["draft", "publish"]).optional().default("publish"),
|
|
1047
|
+
value: managedResourceObject,
|
|
1048
|
+
})
|
|
1049
|
+
.strict()
|
|
1050
|
+
.parse(request.body);
|
|
1051
|
+
return input.service.createManagedResource({ ...params, ...body });
|
|
1052
|
+
});
|
|
1053
|
+
server.put("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
|
|
1054
|
+
const params = z
|
|
1055
|
+
.object({
|
|
1056
|
+
guildId: snowflake,
|
|
1057
|
+
moduleId,
|
|
1058
|
+
resourceType: resourceActionId,
|
|
1059
|
+
resourceId,
|
|
1060
|
+
})
|
|
1061
|
+
.strict()
|
|
1062
|
+
.parse(request.params);
|
|
1063
|
+
const body = mutationContext
|
|
1064
|
+
.extend({
|
|
1065
|
+
expectedRevision: z.number().int().min(0),
|
|
1066
|
+
value: managedResourceObject.optional().default({}),
|
|
1067
|
+
})
|
|
1068
|
+
.strict()
|
|
1069
|
+
.parse(request.body);
|
|
1070
|
+
return input.service.updateManagedResource({ ...params, ...body });
|
|
1071
|
+
});
|
|
1072
|
+
server.delete("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId", async (request) => {
|
|
1073
|
+
const params = z
|
|
1074
|
+
.object({
|
|
1075
|
+
guildId: snowflake,
|
|
1076
|
+
moduleId,
|
|
1077
|
+
resourceType: resourceActionId,
|
|
1078
|
+
resourceId,
|
|
1079
|
+
})
|
|
1080
|
+
.strict()
|
|
1081
|
+
.parse(request.params);
|
|
1082
|
+
const body = mutationContext
|
|
1083
|
+
.extend({ expectedRevision: z.number().int().min(0) })
|
|
1084
|
+
.strict()
|
|
1085
|
+
.parse(request.body);
|
|
1086
|
+
return input.service.deleteManagedResource({ ...params, ...body });
|
|
1087
|
+
});
|
|
1088
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/:resourceId/actions/:actionId", async (request) => {
|
|
1089
|
+
const params = z
|
|
1090
|
+
.object({
|
|
1091
|
+
guildId: snowflake,
|
|
1092
|
+
moduleId,
|
|
1093
|
+
resourceType: resourceActionId,
|
|
1094
|
+
resourceId,
|
|
1095
|
+
actionId: resourceActionId,
|
|
1096
|
+
})
|
|
1097
|
+
.strict()
|
|
1098
|
+
.parse(request.params);
|
|
1099
|
+
const body = mutationContext
|
|
1100
|
+
.extend({
|
|
1101
|
+
expectedRevision: z.number().int().min(0),
|
|
1102
|
+
value: managedResourceObject,
|
|
1103
|
+
})
|
|
1104
|
+
.strict()
|
|
1105
|
+
.parse(request.body);
|
|
1106
|
+
return input.service.executeManagedResourceAction({
|
|
1107
|
+
...params,
|
|
1108
|
+
...body,
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
633
1111
|
server.put("/v1/guilds/:guildId/modules/:moduleId/configuration", async (request) => {
|
|
634
1112
|
const params = z
|
|
635
1113
|
.object({ guildId: snowflake, moduleId })
|
|
@@ -690,6 +1168,8 @@ export function mapBotControlError(error) {
|
|
|
690
1168
|
return { status: 400, message: error.message };
|
|
691
1169
|
if (error instanceof BotControlNotFoundError)
|
|
692
1170
|
return { status: 404, message: error.message };
|
|
1171
|
+
if (error instanceof BotControlForbiddenError)
|
|
1172
|
+
return { status: 403, message: error.message };
|
|
693
1173
|
if (error instanceof OptimisticConcurrencyError)
|
|
694
1174
|
return {
|
|
695
1175
|
status: 409,
|
|
@@ -705,6 +1185,12 @@ class BotControlNotFoundError extends Error {
|
|
|
705
1185
|
this.name = "BotControlNotFoundError";
|
|
706
1186
|
}
|
|
707
1187
|
}
|
|
1188
|
+
class BotControlForbiddenError extends Error {
|
|
1189
|
+
constructor(message = "You no longer have permission to perform this action") {
|
|
1190
|
+
super(message);
|
|
1191
|
+
this.name = "BotControlForbiddenError";
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
708
1194
|
function digest(value) {
|
|
709
1195
|
return createHash("sha256").update(value).digest();
|
|
710
1196
|
}
|
|
@@ -714,6 +1200,181 @@ function isUnknownGuildError(error) {
|
|
|
714
1200
|
"code" in error &&
|
|
715
1201
|
error.code === 10_004);
|
|
716
1202
|
}
|
|
1203
|
+
function isUnknownMemberError(error) {
|
|
1204
|
+
return (typeof error === "object" &&
|
|
1205
|
+
error !== null &&
|
|
1206
|
+
"code" in error &&
|
|
1207
|
+
error.code === 10_007);
|
|
1208
|
+
}
|
|
1209
|
+
function dashboardActionPermissionId(module, actionId) {
|
|
1210
|
+
const namespace = modulePermissionNamespace(module);
|
|
1211
|
+
return selectDeclaredPermission(module, [
|
|
1212
|
+
`${namespace}.${actionId.replaceAll("-", ".")}`,
|
|
1213
|
+
`${namespace}.configure`,
|
|
1214
|
+
]);
|
|
1215
|
+
}
|
|
1216
|
+
function managedResourcePermissionId(module, resourceType, operation, actionId, declaredPermissionId) {
|
|
1217
|
+
if (declaredPermissionId)
|
|
1218
|
+
return declaredPermissionId;
|
|
1219
|
+
const namespace = modulePermissionNamespace(module);
|
|
1220
|
+
const resourcePermission = `resource.${resourceType}`;
|
|
1221
|
+
const candidates = operation === "create" || operation === "preview"
|
|
1222
|
+
? [`${resourcePermission}.create`, `${namespace}.create`]
|
|
1223
|
+
: operation === "update"
|
|
1224
|
+
? [
|
|
1225
|
+
`${resourcePermission}.update`,
|
|
1226
|
+
`${namespace}.edit`,
|
|
1227
|
+
`${namespace}.manage`,
|
|
1228
|
+
]
|
|
1229
|
+
: operation === "delete"
|
|
1230
|
+
? [
|
|
1231
|
+
`${resourcePermission}.delete`,
|
|
1232
|
+
`${namespace}.delete`,
|
|
1233
|
+
`${namespace}.cancel`,
|
|
1234
|
+
]
|
|
1235
|
+
: operation === "action" && actionId
|
|
1236
|
+
? [
|
|
1237
|
+
`${resourcePermission}.${actionId}`,
|
|
1238
|
+
`${resourcePermission}.action.${actionId}`,
|
|
1239
|
+
`${resourcePermission}.action`,
|
|
1240
|
+
`${namespace}.${actionId.replaceAll("-", ".")}`,
|
|
1241
|
+
`${namespace}.manage`,
|
|
1242
|
+
]
|
|
1243
|
+
: [
|
|
1244
|
+
`${resourcePermission}.${operation}`,
|
|
1245
|
+
`${namespace}.read`,
|
|
1246
|
+
`${namespace}.list`,
|
|
1247
|
+
`${namespace}.manage`,
|
|
1248
|
+
`${namespace}.entries.read`,
|
|
1249
|
+
];
|
|
1250
|
+
return selectDeclaredPermission(module, candidates);
|
|
1251
|
+
}
|
|
1252
|
+
function modulePermissionNamespace(module) {
|
|
1253
|
+
return module.manifest.id.startsWith("helyx.")
|
|
1254
|
+
? module.manifest.id.slice("helyx.".length)
|
|
1255
|
+
: module.manifest.id;
|
|
1256
|
+
}
|
|
1257
|
+
function selectDeclaredPermission(module, candidates) {
|
|
1258
|
+
const declared = module.defaultPermissions?.commands ?? {};
|
|
1259
|
+
return (candidates.find((candidate) => candidate in declared) ?? candidates[0]);
|
|
1260
|
+
}
|
|
1261
|
+
function isBoundedManagedResourceValue(value, depth = 0, budget = { keys: 0 }) {
|
|
1262
|
+
if (depth > 8)
|
|
1263
|
+
return false;
|
|
1264
|
+
if (value === null ||
|
|
1265
|
+
typeof value === "boolean" ||
|
|
1266
|
+
(typeof value === "number" && Number.isFinite(value)))
|
|
1267
|
+
return true;
|
|
1268
|
+
if (typeof value === "string")
|
|
1269
|
+
return value.length <= 4_000;
|
|
1270
|
+
if (Array.isArray(value))
|
|
1271
|
+
return (value.length <= 100 &&
|
|
1272
|
+
value.every((item) => isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
1273
|
+
if (typeof value !== "object")
|
|
1274
|
+
return false;
|
|
1275
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1276
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
1277
|
+
return false;
|
|
1278
|
+
const entries = Object.entries(value);
|
|
1279
|
+
budget.keys += entries.length;
|
|
1280
|
+
return (entries.length <= 100 &&
|
|
1281
|
+
budget.keys <= 500 &&
|
|
1282
|
+
entries.every(([key, item]) => key.length >= 1 &&
|
|
1283
|
+
key.length <= 100 &&
|
|
1284
|
+
isBoundedManagedResourceValue(item, depth + 1, budget)));
|
|
1285
|
+
}
|
|
1286
|
+
function hasBoundedManagedResourceBytes(value) {
|
|
1287
|
+
return (new TextEncoder().encode(JSON.stringify(value)).byteLength <= 48 * 1_024);
|
|
1288
|
+
}
|
|
1289
|
+
function normalizeManagedResourceDetail(value, attributeKeys) {
|
|
1290
|
+
return {
|
|
1291
|
+
id: value.id,
|
|
1292
|
+
revision: value.revision,
|
|
1293
|
+
status: value.status,
|
|
1294
|
+
title: value.title,
|
|
1295
|
+
...(value.description === undefined
|
|
1296
|
+
? {}
|
|
1297
|
+
: { description: value.description }),
|
|
1298
|
+
...(value.attributes === undefined || attributeKeys.length === 0
|
|
1299
|
+
? {}
|
|
1300
|
+
: {
|
|
1301
|
+
attributes: projectManagedResourceObject(value.attributes, attributeKeys),
|
|
1302
|
+
}),
|
|
1303
|
+
updatedAt: value.updatedAt,
|
|
1304
|
+
value: value.value,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function normalizeManagedResourcePage(value, attributeKeys) {
|
|
1308
|
+
return {
|
|
1309
|
+
items: value.items.map((item) => ({
|
|
1310
|
+
id: item.id,
|
|
1311
|
+
revision: item.revision,
|
|
1312
|
+
status: item.status,
|
|
1313
|
+
title: item.title,
|
|
1314
|
+
...(item.description === undefined
|
|
1315
|
+
? {}
|
|
1316
|
+
: { description: item.description }),
|
|
1317
|
+
...(item.attributes === undefined || attributeKeys.length === 0
|
|
1318
|
+
? {}
|
|
1319
|
+
: {
|
|
1320
|
+
attributes: projectManagedResourceObject(item.attributes, attributeKeys),
|
|
1321
|
+
}),
|
|
1322
|
+
updatedAt: item.updatedAt,
|
|
1323
|
+
})),
|
|
1324
|
+
...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function normalizeManagedResourceRelationPage(value, valueKeys) {
|
|
1328
|
+
return {
|
|
1329
|
+
items: value.items.map((item) => ({
|
|
1330
|
+
id: item.id,
|
|
1331
|
+
value: projectManagedResourceObject(item.value, valueKeys),
|
|
1332
|
+
})),
|
|
1333
|
+
...(value.nextCursor === undefined ? {} : { nextCursor: value.nextCursor }),
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function projectManagedResourceObject(value, declaredKeys) {
|
|
1337
|
+
const allowed = new Set(declaredKeys);
|
|
1338
|
+
return Object.fromEntries(Object.entries(value)
|
|
1339
|
+
.filter(([key]) => allowed.has(key))
|
|
1340
|
+
.map(([key, item]) => [key, structuredClone(item)]));
|
|
1341
|
+
}
|
|
1342
|
+
function managedResourceColumnKeys(module, resourceType) {
|
|
1343
|
+
return (module.manifest.dashboard?.resources
|
|
1344
|
+
?.find(({ id }) => id === resourceType)
|
|
1345
|
+
?.columns?.map(({ key }) => key) ?? []);
|
|
1346
|
+
}
|
|
1347
|
+
function validateManagedResourceActionValue(inputs, value) {
|
|
1348
|
+
const declared = new Map(inputs.map((input) => [input.key, input]));
|
|
1349
|
+
for (const key of Object.keys(value))
|
|
1350
|
+
if (!declared.has(key))
|
|
1351
|
+
throw new DashboardActionValidationError(`The action value "${key}" is not declared by the module.`);
|
|
1352
|
+
for (const input of inputs) {
|
|
1353
|
+
const item = value[input.key];
|
|
1354
|
+
if (item === undefined) {
|
|
1355
|
+
if (input.required)
|
|
1356
|
+
throw new DashboardActionValidationError(`${input.label} is required for this action.`);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const valid = input.type === "boolean"
|
|
1360
|
+
? typeof item === "boolean"
|
|
1361
|
+
: input.type === "integer"
|
|
1362
|
+
? typeof item === "number" &&
|
|
1363
|
+
Number.isInteger(item) &&
|
|
1364
|
+
item >= input.minimum &&
|
|
1365
|
+
item <= input.maximum
|
|
1366
|
+
: input.type === "select"
|
|
1367
|
+
? typeof item === "string" &&
|
|
1368
|
+
input.options.some(({ value: option }) => option === item)
|
|
1369
|
+
: input.type === "discord-channel" || input.type === "discord-role"
|
|
1370
|
+
? typeof item === "string" && snowflake.safeParse(item).success
|
|
1371
|
+
: typeof item === "string" &&
|
|
1372
|
+
item.length >= input.minLength &&
|
|
1373
|
+
item.length <= input.maxLength;
|
|
1374
|
+
if (!valid)
|
|
1375
|
+
throw new DashboardActionValidationError(`${input.label} is not a valid action value.`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
717
1378
|
function mapDashboardModule(module) {
|
|
718
1379
|
const dashboard = module.manifest.dashboard;
|
|
719
1380
|
return {
|
|
@@ -730,12 +1391,15 @@ function mapDashboardModule(module) {
|
|
|
730
1391
|
commands: dashboardCommands(module),
|
|
731
1392
|
logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
|
|
732
1393
|
actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
|
|
1394
|
+
resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
|
|
733
1395
|
};
|
|
734
1396
|
}
|
|
735
1397
|
function dashboardCommands(module) {
|
|
736
|
-
|
|
1398
|
+
const commands = (module.interactions?.commands ?? []).flatMap((command) => command.subcommands?.length
|
|
737
1399
|
? command.subcommands.map((subcommand) => {
|
|
738
|
-
const id =
|
|
1400
|
+
const id = subcommand.permissionId ??
|
|
1401
|
+
command.permissionId ??
|
|
1402
|
+
`${command.name}.${subcommand.name}`;
|
|
739
1403
|
return {
|
|
740
1404
|
id,
|
|
741
1405
|
name: `/${command.name} ${subcommand.name}`,
|
|
@@ -747,7 +1411,7 @@ function dashboardCommands(module) {
|
|
|
747
1411
|
})
|
|
748
1412
|
: [
|
|
749
1413
|
{
|
|
750
|
-
id: command.name,
|
|
1414
|
+
id: command.permissionId ?? command.name,
|
|
751
1415
|
name: `/${command.name}`,
|
|
752
1416
|
description: command.description,
|
|
753
1417
|
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[command.name] ??
|
|
@@ -755,6 +1419,9 @@ function dashboardCommands(module) {
|
|
|
755
1419
|
emptyPolicy()).preset,
|
|
756
1420
|
},
|
|
757
1421
|
]);
|
|
1422
|
+
return [
|
|
1423
|
+
...new Map(commands.map((command) => [command.id, command])).values(),
|
|
1424
|
+
];
|
|
758
1425
|
}
|
|
759
1426
|
function toDashboardPolicy(policy) {
|
|
760
1427
|
const preset = policy.everyone
|