@helyx/module-reactionroles 1.0.15 → 1.0.18

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +37 -53
  3. package/dist/builder-interactions.d.ts +11 -0
  4. package/dist/builder-interactions.js +293 -0
  5. package/dist/builder-presentation.d.ts +30 -0
  6. package/dist/builder-presentation.js +343 -0
  7. package/dist/builder.d.ts +3 -0
  8. package/dist/builder.js +22 -16
  9. package/dist/cleanup-repository.d.ts +24 -0
  10. package/dist/cleanup-repository.js +118 -0
  11. package/dist/constants.d.ts +3 -0
  12. package/dist/constants.js +4 -1
  13. package/dist/health.d.ts +16 -0
  14. package/dist/health.js +85 -0
  15. package/dist/index.js +66 -1147
  16. package/dist/logging.d.ts +33 -0
  17. package/dist/logging.js +40 -0
  18. package/dist/managed-resource.d.ts +2 -0
  19. package/dist/managed-resource.js +72 -0
  20. package/dist/member-interactions.d.ts +27 -0
  21. package/dist/member-interactions.js +360 -0
  22. package/dist/module-lifecycle.d.ts +3 -0
  23. package/dist/module-lifecycle.js +48 -0
  24. package/dist/panel-cache.d.ts +30 -0
  25. package/dist/panel-cache.js +197 -0
  26. package/dist/panel-create.d.ts +10 -0
  27. package/dist/panel-create.js +80 -0
  28. package/dist/panel-lifecycle.d.ts +47 -0
  29. package/dist/panel-lifecycle.js +272 -0
  30. package/dist/panel-managed.d.ts +14 -0
  31. package/dist/panel-managed.js +27 -0
  32. package/dist/panel-service.d.ts +13 -36
  33. package/dist/panel-service.js +48 -722
  34. package/dist/panel-support.d.ts +35 -0
  35. package/dist/panel-support.js +291 -0
  36. package/dist/permissions.d.ts +2 -0
  37. package/dist/permissions.js +8 -0
  38. package/dist/privacy.d.ts +5 -0
  39. package/dist/privacy.js +12 -0
  40. package/dist/record-mapping.d.ts +28 -0
  41. package/dist/record-mapping.js +161 -0
  42. package/dist/record-schema.d.ts +203 -0
  43. package/dist/record-schema.js +137 -0
  44. package/dist/records.d.ts +9 -201
  45. package/dist/records.js +32 -383
  46. package/dist/runtime-state.d.ts +9 -0
  47. package/dist/runtime-state.js +30 -0
  48. package/manifest.json +1 -1
  49. package/package.json +3 -3
@@ -0,0 +1,35 @@
1
+ import { type DiscordResourceInspectionService, type ManagedResourceContext, type ManagedResourceDetail, type ManagedResourceValue, type MessageBuilderResult, type ServiceAccess } from "@helyx/sdk";
2
+ import { type PanelDraft, type PanelEntryDraft } from "./builder.js";
3
+ import type { PanelMutationContext } from "./panel-service.js";
4
+ import type { StoredPanel } from "./records.js";
5
+ export declare function managedContext(context: ManagedResourceContext): PanelMutationContext;
6
+ export declare function parseManagedDraft(value: Readonly<Record<string, ManagedResourceValue>>): PanelDraft;
7
+ export declare function toDetail(panel: StoredPanel): ManagedResourceDetail;
8
+ export declare function toSummary(panel: StoredPanel): {
9
+ id: string;
10
+ revision: number;
11
+ status: import("./record-schema.js").PanelStatus;
12
+ title: string;
13
+ description: string;
14
+ updatedAt: string;
15
+ };
16
+ export declare function validateDiscordResources(services: ServiceAccess, guildId: string, actorUserId: string, draft: PanelDraft): Promise<void>;
17
+ export declare function channelSupportsPanel(channel: Awaited<ReturnType<DiscordResourceInspectionService["inspectChannel"]>>, panel: Pick<PanelDraft, "messageStyle" | "controlType" | "persistentReactions">): boolean;
18
+ export declare function prepareEntries(entries: readonly PanelEntryDraft[], existing?: StoredPanel): {
19
+ entryId: string;
20
+ roleId: string;
21
+ nameplate: string;
22
+ emoji: import("./builder.js").CanonicalEmoji | null;
23
+ buttonStyle: import("./builder.js").PanelButtonStyle | null;
24
+ }[];
25
+ export declare function seedReactions(context: Pick<PanelMutationContext, "services" | "addChannelMessageReaction">, draft: PanelDraft, messageId: string): Promise<void>;
26
+ export declare function buildPanel(services: ServiceAccess, draft: PanelDraft): MessageBuilderResult;
27
+ export declare function requiresReplacement(current: StoredPanel, draft: PanelDraft): boolean;
28
+ export declare function emitLog(services: ServiceAccess, guildId: string, input: {
29
+ eventId: string;
30
+ summary: string;
31
+ panelId: string;
32
+ }): Promise<void>;
33
+ export declare function appendPanelAudit(context: PanelMutationContext, action: string, panel: StoredPanel, result?: "success" | "cleanup_pending"): Promise<void>;
34
+ export declare function encodeCursor(cursor: readonly (string | boolean | number | Date)[]): string;
35
+ export declare function decodeCursor(cursor: string): readonly (string | boolean | number | Date)[];
@@ -0,0 +1,291 @@
1
+ import { DashboardActionValidationError, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { randomUUID } from "node:crypto";
3
+ import { canonicaliseEmoji, panelMessageDraft, PanelDraftValidationError, validatePanelDraft, } from "./builder.js";
4
+ import { REACTION_ROLES_MODULE_ID } from "./constants.js";
5
+ export function managedContext(context) {
6
+ return {
7
+ guildId: context.guildId,
8
+ actorUserId: context.actor.userId,
9
+ source: "dashboard",
10
+ services: context.services,
11
+ correlationId: context.correlationId,
12
+ sendToChannel: (channelId, response) => context.sendToChannel(channelId, response),
13
+ deleteChannelMessage: (channelId, messageId) => context.deleteChannelMessage(channelId, messageId),
14
+ editChannelMessage: (channelId, messageId, response) => context.editChannelMessage(channelId, messageId, response),
15
+ addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => context.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
16
+ };
17
+ }
18
+ export function parseManagedDraft(value) {
19
+ const entriesValue = value.entries;
20
+ if (!Array.isArray(entriesValue))
21
+ throw new DashboardActionValidationError("Add at least one role entry before publishing");
22
+ const entries = entriesValue.map((item, index) => {
23
+ if (!isManagedObject(item))
24
+ throw new DashboardActionValidationError(`Role entry ${index + 1} is invalid`);
25
+ return {
26
+ entryId: optionalString(item.entryId) ??
27
+ `entry-${randomUUID().replaceAll("-", "")}`,
28
+ roleId: requiredManagedString(item.roleId, "Role"),
29
+ nameplate: requiredManagedString(item.nameplate, "Role label"),
30
+ ...(optionalString(item.emoji)
31
+ ? { emoji: optionalString(item.emoji) }
32
+ : {}),
33
+ ...(optionalString(item.buttonStyle)
34
+ ? {
35
+ buttonStyle: requiredLiteral(optionalString(item.buttonStyle), ["primary", "secondary", "success", "danger"], "button colour"),
36
+ }
37
+ : {}),
38
+ };
39
+ });
40
+ try {
41
+ return validatePanelDraft({
42
+ channelId: requiredManagedString(value.channelId, "Destination channel"),
43
+ title: requiredManagedString(value.title, "Title"),
44
+ messageBody: requiredManagedString(value.messageBody, "Message"),
45
+ messageStyle: requiredLiteral(requiredManagedString(value.messageStyle, "Message style"), ["text", "embed"], "message style"),
46
+ controlType: requiredLiteral(requiredManagedString(value.controlType, "Role controls"), ["buttons", "dropdown", "reactions"], "role controls"),
47
+ selectionMode: requiredLiteral(requiredManagedString(value.selectionMode, "Selection mode"), ["multiple", "one_only"], "selection mode"),
48
+ persistentReactions: typeof value.persistentReactions === "boolean"
49
+ ? value.persistentReactions
50
+ : true,
51
+ ...(typeof value.embedColour === "number"
52
+ ? { embedColour: value.embedColour }
53
+ : {}),
54
+ ...(optionalString(value.dropdownPlaceholder)
55
+ ? { dropdownPlaceholder: optionalString(value.dropdownPlaceholder) }
56
+ : {}),
57
+ entries,
58
+ });
59
+ }
60
+ catch (error) {
61
+ if (error instanceof DashboardActionValidationError)
62
+ throw error;
63
+ if (error instanceof PanelDraftValidationError)
64
+ throw new DashboardActionValidationError(error.message);
65
+ throw new DashboardActionValidationError("Role panel draft is invalid.");
66
+ }
67
+ }
68
+ export function toDetail(panel) {
69
+ return {
70
+ ...toSummary(panel),
71
+ value: {
72
+ channelId: panel.channelId,
73
+ title: panel.title,
74
+ messageBody: panel.messageBody,
75
+ messageStyle: panel.messageStyle,
76
+ controlType: panel.controlType,
77
+ selectionMode: panel.selectionMode,
78
+ persistentReactions: panel.persistentReactions,
79
+ ...(panel.embedColour === undefined
80
+ ? {}
81
+ : { embedColour: panel.embedColour }),
82
+ ...(panel.dropdownPlaceholder === undefined
83
+ ? {}
84
+ : { dropdownPlaceholder: panel.dropdownPlaceholder }),
85
+ entries: panel.entries.map((entry) => ({
86
+ entryId: entry.entryId,
87
+ roleId: entry.roleId,
88
+ nameplate: entry.nameplate,
89
+ ...(entry.emoji ? { emoji: entry.emoji } : {}),
90
+ ...(entry.buttonStyle ? { buttonStyle: entry.buttonStyle } : {}),
91
+ })),
92
+ },
93
+ };
94
+ }
95
+ export function toSummary(panel) {
96
+ return {
97
+ id: panel.panelId,
98
+ revision: panel.revision,
99
+ status: panel.status,
100
+ title: panel.title,
101
+ description: `${panel.entries.length} role ${panel.entries.length === 1 ? "entry" : "entries"} · ${panel.controlType}`,
102
+ updatedAt: panel.updatedAt.toISOString(),
103
+ };
104
+ }
105
+ export async function validateDiscordResources(services, guildId, actorUserId, draft) {
106
+ const resources = services.get(HELYX_SERVICE_NAMES.discordResources);
107
+ const channel = await resources.inspectChannel({
108
+ guildId,
109
+ channelId: draft.channelId,
110
+ });
111
+ if (!channel.exists || !channel.guildMatches || !channel.viewable)
112
+ throw new DashboardActionValidationError("Helyx cannot view the selected destination channel");
113
+ if (!channel.sendMessages)
114
+ throw new DashboardActionValidationError("Helyx cannot send messages in the selected destination channel");
115
+ if (draft.messageStyle === "embed" &&
116
+ !channel.effectivePermissionNames.includes("EMBED_LINKS"))
117
+ throw new DashboardActionValidationError("Helyx needs Embed Links in the selected destination channel");
118
+ if (draft.controlType === "reactions" &&
119
+ (!channel.addReactions || !channel.readMessageHistory))
120
+ throw new DashboardActionValidationError("Emoji-reaction panels need Add Reactions and Read Message History in the selected channel");
121
+ if (draft.controlType === "reactions" &&
122
+ !draft.persistentReactions &&
123
+ !channel.effectivePermissionNames.includes("MANAGE_MESSAGES"))
124
+ throw new DashboardActionValidationError("Non-persistent emoji reactions need Manage Messages in the selected channel");
125
+ for (const entry of draft.entries) {
126
+ const role = await resources.inspectRole({
127
+ guildId,
128
+ roleId: entry.roleId,
129
+ actorUserId,
130
+ });
131
+ validateRole(entry.nameplate, role);
132
+ if (entry.emoji) {
133
+ const emoji = await resources.inspectEmoji({
134
+ guildId,
135
+ emojiIdentifier: entry.emoji,
136
+ });
137
+ if (!emoji.exists || !emoji.guildMatches || !emoji.usable)
138
+ throw new DashboardActionValidationError(`The emoji for "${entry.nameplate}" is not available in this server`);
139
+ }
140
+ }
141
+ }
142
+ function validateRole(nameplate, role) {
143
+ if (!role.exists || !role.guildMatches)
144
+ throw new DashboardActionValidationError(`The role for "${nameplate}" is no longer available in this server`);
145
+ if (role.everyone)
146
+ throw new DashboardActionValidationError(`@everyone cannot be made self-assignable for "${nameplate}"`);
147
+ if (role.managed)
148
+ throw new DashboardActionValidationError(`The role for "${nameplate}" is managed by Discord or an integration and cannot be assigned`);
149
+ if (role.dangerousPermissionNames.length > 0)
150
+ throw new DashboardActionValidationError(`The role for "${nameplate}" grants administrative or moderation permissions and cannot be self-assigned`);
151
+ if (role.hierarchy === "missing-manage-roles")
152
+ throw new DashboardActionValidationError(`Helyx needs Manage Roles before it can assign "${nameplate}"`);
153
+ if (role.hierarchy === "above-helyx" || !role.assignable)
154
+ throw new DashboardActionValidationError(`Move Helyx's highest role above the role for "${nameplate}", then try again`);
155
+ if (role.hierarchy === "above-actor" || !role.actorAssignable)
156
+ throw new DashboardActionValidationError(`Your highest role must be above the role for "${nameplate}"`);
157
+ }
158
+ export function channelSupportsPanel(channel, panel) {
159
+ return (channel.exists &&
160
+ channel.guildMatches &&
161
+ channel.viewable &&
162
+ channel.sendMessages &&
163
+ (panel.messageStyle !== "embed" ||
164
+ channel.effectivePermissionNames.includes("EMBED_LINKS")) &&
165
+ (panel.controlType !== "reactions" ||
166
+ (channel.addReactions &&
167
+ channel.readMessageHistory &&
168
+ (panel.persistentReactions ||
169
+ channel.effectivePermissionNames.includes("MANAGE_MESSAGES")))));
170
+ }
171
+ export function prepareEntries(entries, existing) {
172
+ return entries.map((entry) => {
173
+ const prior = existing?.entries.find((candidate) => candidate.roleId === entry.roleId);
174
+ return {
175
+ entryId: prior?.entryId ?? entry.entryId,
176
+ roleId: entry.roleId,
177
+ nameplate: entry.nameplate,
178
+ emoji: entry.emoji ? canonicaliseEmoji(entry.emoji) : null,
179
+ buttonStyle: entry.buttonStyle ?? null,
180
+ };
181
+ });
182
+ }
183
+ export async function seedReactions(context, draft, messageId) {
184
+ for (const emojiIdentifier of buildPanel(context.services, draft)
185
+ .reactionSeeds)
186
+ await context.addChannelMessageReaction(draft.channelId, messageId, emojiIdentifier);
187
+ }
188
+ export function buildPanel(services, draft) {
189
+ return services
190
+ .get(HELYX_SERVICE_NAMES.messageBuilder)
191
+ .build(panelMessageDraft(draft));
192
+ }
193
+ export function requiresReplacement(current, draft) {
194
+ if (current.channelId !== draft.channelId)
195
+ return true;
196
+ if (current.controlType !== "reactions" && draft.controlType !== "reactions")
197
+ return false;
198
+ if (current.controlType !== draft.controlType)
199
+ return true;
200
+ return (current.entries.some((entry, index) => {
201
+ const next = draft.entries[index];
202
+ return (!next || entry.roleId !== next.roleId || entry.emoji !== next.emoji);
203
+ }) || current.entries.length !== draft.entries.length);
204
+ }
205
+ export async function emitLog(services, guildId, input) {
206
+ if (!services.has(HELYX_SERVICE_NAMES.moduleLogging))
207
+ return;
208
+ await services
209
+ .get(HELYX_SERVICE_NAMES.moduleLogging)
210
+ .emit({
211
+ guildId,
212
+ moduleId: REACTION_ROLES_MODULE_ID,
213
+ eventId: input.eventId,
214
+ summary: input.summary,
215
+ details: [{ label: "Panel", value: input.panelId }],
216
+ })
217
+ .catch(() => undefined);
218
+ }
219
+ export async function appendPanelAudit(context, action, panel, result = "success") {
220
+ try {
221
+ const audit = context.services.get(HELYX_SERVICE_NAMES.audit);
222
+ await audit.append({
223
+ guildId: context.guildId,
224
+ actorUserId: context.actorUserId,
225
+ action,
226
+ source: context.source,
227
+ ...(context.correlationId
228
+ ? { correlationId: context.correlationId }
229
+ : {}),
230
+ moduleId: REACTION_ROLES_MODULE_ID,
231
+ targetType: "reaction_role_panel",
232
+ targetId: panel.panelId,
233
+ metadata: {
234
+ channelId: panel.channelId,
235
+ revision: panel.revision,
236
+ controlType: panel.controlType,
237
+ status: panel.status,
238
+ entryCount: panel.entries.length,
239
+ result,
240
+ },
241
+ });
242
+ }
243
+ catch {
244
+ // The Discord/database mutation is already committed. Audit delivery must
245
+ // not misreport that customer operation as failed.
246
+ }
247
+ }
248
+ export function encodeCursor(cursor) {
249
+ return Buffer.from(JSON.stringify(cursor.map((value) => value instanceof Date ? { date: value.toISOString() } : { value }))).toString("base64url");
250
+ }
251
+ export function decodeCursor(cursor) {
252
+ try {
253
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
254
+ if (!Array.isArray(parsed) || parsed.length !== 2)
255
+ throw new Error();
256
+ return parsed.map((item) => {
257
+ if (!isManagedObject(item))
258
+ throw new Error();
259
+ if (typeof item.date === "string") {
260
+ const date = new Date(item.date);
261
+ if (Number.isNaN(date.getTime()))
262
+ throw new Error();
263
+ return date;
264
+ }
265
+ if (typeof item.value !== "string" &&
266
+ typeof item.value !== "boolean" &&
267
+ typeof item.value !== "number")
268
+ throw new Error();
269
+ return item.value;
270
+ });
271
+ }
272
+ catch {
273
+ throw new DashboardActionValidationError("The role-panel page cursor is invalid");
274
+ }
275
+ }
276
+ function isManagedObject(value) {
277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
278
+ }
279
+ function requiredManagedString(value, label) {
280
+ if (typeof value !== "string")
281
+ throw new DashboardActionValidationError(`${label} is required`);
282
+ return value;
283
+ }
284
+ function optionalString(value) {
285
+ return typeof value === "string" && value.trim() ? value : undefined;
286
+ }
287
+ function requiredLiteral(value, allowed, label) {
288
+ if (!allowed.includes(value))
289
+ throw new DashboardActionValidationError(`Choose a valid ${label}`);
290
+ return value;
291
+ }
@@ -0,0 +1,2 @@
1
+ import type { PermissionPolicy } from "@helyx/sdk";
2
+ export declare const reactionRoleManagerPolicy: PermissionPolicy;
@@ -0,0 +1,8 @@
1
+ export const reactionRoleManagerPolicy = {
2
+ everyone: false,
3
+ requiredPermissions: ["MANAGE_GUILD"],
4
+ allowedRoleIds: [],
5
+ deniedRoleIds: [],
6
+ allowedUserIds: [],
7
+ deniedUserIds: [],
8
+ };
@@ -0,0 +1,5 @@
1
+ import { createPrivacyProvider, getModulePrivacyDeclaration } from "@helyx/sdk";
2
+ export declare function createReactionRolePrivacy(manifest: Parameters<typeof getModulePrivacyDeclaration>[0]): {
3
+ providerVersion: number;
4
+ create: (executor: Parameters<typeof createPrivacyProvider>[0]["executor"]) => import("@helyx/sdk").PrivacyProvider;
5
+ };
@@ -0,0 +1,12 @@
1
+ import { createPrivacyProvider, getModulePrivacyDeclaration } from "@helyx/sdk";
2
+ import { REACTION_ROLES_MODULE_ID } from "./constants.js";
3
+ export function createReactionRolePrivacy(manifest) {
4
+ return {
5
+ providerVersion: 1,
6
+ create: (executor) => createPrivacyProvider({
7
+ id: REACTION_ROLES_MODULE_ID,
8
+ declaration: getModulePrivacyDeclaration(manifest),
9
+ executor,
10
+ }),
11
+ };
12
+ }
@@ -0,0 +1,28 @@
1
+ import type { ModuleRecordValue, ModuleRecordWriteValue } from "@helyx/sdk";
2
+ import type { CanonicalEmoji, PanelButtonStyle, PanelDraft } from "./builder.js";
3
+ import type { PanelStatus, StoredPanel } from "./record-schema.js";
4
+ export declare function panelValues(input: {
5
+ panelId: string;
6
+ guildId: string;
7
+ actorUserId: string;
8
+ draft: PanelDraft;
9
+ messageId?: string;
10
+ status: PanelStatus;
11
+ revision: number;
12
+ now: Date;
13
+ }): Record<string, ModuleRecordWriteValue>;
14
+ export declare function entryValues(panelId: string, entry: {
15
+ entryId: string;
16
+ roleId: string;
17
+ nameplate: string;
18
+ emoji: CanonicalEmoji | null;
19
+ buttonStyle: PanelButtonStyle | null;
20
+ }, position: number, now: Date): Record<string, ModuleRecordWriteValue>;
21
+ export declare function mapPanel(row: Readonly<Record<string, ModuleRecordValue>>, entries: readonly Readonly<Record<string, ModuleRecordValue>>[]): StoredPanel;
22
+ export declare function requiredString(row: Readonly<Record<string, ModuleRecordValue>>, field: string): string;
23
+ export declare function nullableString(row: Readonly<Record<string, ModuleRecordValue>>, field: string): string | null;
24
+ export declare function requiredNumber(row: Readonly<Record<string, ModuleRecordValue>>, field: string): number;
25
+ export declare function optionalBoolean(row: Readonly<Record<string, ModuleRecordValue>>, field: string, fallback: boolean): boolean;
26
+ export declare function requiredDate(row: Readonly<Record<string, ModuleRecordValue>>, field: string): Date;
27
+ export declare function requiredEnum<const T extends string>(row: Readonly<Record<string, ModuleRecordValue>>, field: string, values: readonly T[]): T;
28
+ export declare function requiredLiteral<const T extends string>(value: string, values: readonly T[]): T;
@@ -0,0 +1,161 @@
1
+ export function panelValues(input) {
2
+ return {
3
+ panel_id: input.panelId,
4
+ guild_id: input.guildId,
5
+ channel_id: input.draft.channelId,
6
+ ...(input.messageId ? { message_id: input.messageId } : {}),
7
+ title: input.draft.title,
8
+ message_body: input.draft.messageBody,
9
+ message_style: input.draft.messageStyle,
10
+ control_type: input.draft.controlType,
11
+ selection_mode: input.draft.selectionMode,
12
+ persistent_reactions: input.draft.persistentReactions,
13
+ ...(input.draft.embedColour === undefined
14
+ ? {}
15
+ : { embed_colour: input.draft.embedColour }),
16
+ ...(input.draft.dropdownPlaceholder === undefined
17
+ ? {}
18
+ : { dropdown_placeholder: input.draft.dropdownPlaceholder }),
19
+ status: input.status,
20
+ revision: input.revision,
21
+ created_by_user_id: input.actorUserId,
22
+ updated_by_user_id: input.actorUserId,
23
+ created_at: input.now,
24
+ updated_at: input.now,
25
+ };
26
+ }
27
+ export function entryValues(panelId, entry, position, now) {
28
+ return {
29
+ entry_id: entry.entryId,
30
+ panel_id: panelId,
31
+ position,
32
+ role_id: entry.roleId,
33
+ nameplate: entry.nameplate,
34
+ ...(entry.emoji
35
+ ? {
36
+ emoji_kind: entry.emoji.kind,
37
+ emoji_name: entry.emoji.name,
38
+ ...(entry.emoji.id ? { emoji_id: entry.emoji.id } : {}),
39
+ emoji_animated: entry.emoji.animated,
40
+ }
41
+ : {}),
42
+ ...(entry.buttonStyle ? { button_style: entry.buttonStyle } : {}),
43
+ created_at: now,
44
+ updated_at: now,
45
+ };
46
+ }
47
+ export function mapPanel(row, entries) {
48
+ return {
49
+ panelId: requiredString(row, "panel_id"),
50
+ guildId: requiredString(row, "guild_id"),
51
+ channelId: requiredString(row, "channel_id"),
52
+ messageId: nullableString(row, "message_id"),
53
+ title: requiredString(row, "title"),
54
+ messageBody: requiredString(row, "message_body"),
55
+ messageStyle: requiredEnum(row, "message_style", ["text", "embed"]),
56
+ controlType: requiredEnum(row, "control_type", [
57
+ "buttons",
58
+ "dropdown",
59
+ "reactions",
60
+ ]),
61
+ selectionMode: requiredEnum(row, "selection_mode", [
62
+ "multiple",
63
+ "one_only",
64
+ ]),
65
+ persistentReactions: optionalBoolean(row, "persistent_reactions", true),
66
+ ...(row.embed_colour === null
67
+ ? {}
68
+ : { embedColour: requiredNumber(row, "embed_colour") }),
69
+ ...(row.dropdown_placeholder === null
70
+ ? {}
71
+ : {
72
+ dropdownPlaceholder: requiredString(row, "dropdown_placeholder"),
73
+ }),
74
+ entries: entries.map((entry) => {
75
+ const emojiKind = nullableString(entry, "emoji_kind");
76
+ const emojiName = nullableString(entry, "emoji_name");
77
+ const emojiId = nullableString(entry, "emoji_id");
78
+ const emoji = emojiKind && emojiName
79
+ ? {
80
+ kind: requiredLiteral(emojiKind, ["unicode", "custom"]),
81
+ name: emojiName,
82
+ ...(emojiId ? { id: emojiId } : {}),
83
+ animated: entry.emoji_animated === true,
84
+ identifier: emojiId ? `${emojiName}:${emojiId}` : emojiName,
85
+ key: emojiId ? `custom:${emojiId}` : `unicode:${emojiName}`,
86
+ }
87
+ : null;
88
+ return {
89
+ entryId: requiredString(entry, "entry_id"),
90
+ roleId: requiredString(entry, "role_id"),
91
+ nameplate: requiredString(entry, "nameplate"),
92
+ ...(emoji ? { emoji: emoji.identifier } : {}),
93
+ ...(entry.button_style
94
+ ? {
95
+ buttonStyle: requiredEnum(entry, "button_style", [
96
+ "primary",
97
+ "secondary",
98
+ "success",
99
+ "danger",
100
+ ]),
101
+ }
102
+ : {}),
103
+ };
104
+ }),
105
+ status: requiredEnum(row, "status", [
106
+ "pending",
107
+ "active",
108
+ "disabled",
109
+ "message_missing",
110
+ "needs_attention",
111
+ "cleanup_pending",
112
+ "deleting",
113
+ ]),
114
+ revision: requiredNumber(row, "revision"),
115
+ createdByUserId: nullableString(row, "created_by_user_id"),
116
+ updatedByUserId: nullableString(row, "updated_by_user_id"),
117
+ createdAt: requiredDate(row, "created_at"),
118
+ updatedAt: requiredDate(row, "updated_at"),
119
+ };
120
+ }
121
+ export function requiredString(row, field) {
122
+ const value = row[field];
123
+ if (typeof value !== "string" || value.length === 0)
124
+ throw new Error(`Invalid Reaction Roles record field: ${field}`);
125
+ return value;
126
+ }
127
+ export function nullableString(row, field) {
128
+ const value = row[field];
129
+ if (value === null || value === undefined)
130
+ return null;
131
+ return requiredString(row, field);
132
+ }
133
+ export function requiredNumber(row, field) {
134
+ const value = Number(row[field]);
135
+ if (!Number.isSafeInteger(value))
136
+ throw new Error(`Invalid Reaction Roles record field: ${field}`);
137
+ return value;
138
+ }
139
+ export function optionalBoolean(row, field, fallback) {
140
+ const value = row[field];
141
+ if (value === null || value === undefined)
142
+ return fallback;
143
+ if (typeof value !== "boolean")
144
+ throw new Error(`Invalid Reaction Roles record field: ${field}`);
145
+ return value;
146
+ }
147
+ export function requiredDate(row, field) {
148
+ const value = row[field];
149
+ const date = value instanceof Date ? value : new Date(String(value));
150
+ if (Number.isNaN(date.getTime()))
151
+ throw new Error(`Invalid Reaction Roles record field: ${field}`);
152
+ return date;
153
+ }
154
+ export function requiredEnum(row, field, values) {
155
+ return requiredLiteral(requiredString(row, field), values);
156
+ }
157
+ export function requiredLiteral(value, values) {
158
+ if (!values.includes(value))
159
+ throw new Error("Invalid Reaction Roles record enum");
160
+ return value;
161
+ }