@helyx/module-reactionroles 1.0.12 → 1.0.14

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @helyx/module-reactionroles
2
2
 
3
+ ## 1.0.14
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove the Reaction Roles command root now that the dashboard Panels workspace owns panel creation and management.
8
+
9
+ ## 1.0.13
10
+
11
+ ### Patch Changes
12
+
13
+ - Consolidate the module database migration history into a clean one-file baseline for the planned database rebuild.
14
+
3
15
  ## 1.0.12
4
16
 
5
17
  ### Patch Changes
package/dist/index.js CHANGED
@@ -90,103 +90,6 @@ export const reactionRolesModule = defineModule({
90
90
  },
91
91
  records: reactionRoleRecords,
92
92
  interactions: {
93
- commands: [
94
- {
95
- name: "reaction-role",
96
- description: "Create and manage Helyx role panels.",
97
- registration: "global",
98
- contexts: ["guild"],
99
- integrationTypes: ["guild_install"],
100
- subcommands: [
101
- {
102
- name: "create",
103
- acknowledgement: "immediate-modal",
104
- description: "Open the guided Reaction Roles builder.",
105
- permissionId: "reactionroles.create",
106
- options: [
107
- {
108
- type: "channel",
109
- name: "channel",
110
- description: "Prefill the destination channel.",
111
- channelTypes: ["guild_text", "guild_announcement"],
112
- },
113
- {
114
- type: "string",
115
- name: "style",
116
- description: "Prefill the public message style.",
117
- choices: [
118
- { name: "Text", value: "text" },
119
- { name: "Embed", value: "embed" },
120
- ],
121
- },
122
- {
123
- type: "string",
124
- name: "control",
125
- description: "Prefill how members choose roles.",
126
- choices: [
127
- { name: "Buttons", value: "buttons" },
128
- { name: "Dropdown", value: "dropdown" },
129
- { name: "Emoji reactions", value: "reactions" },
130
- ],
131
- },
132
- {
133
- type: "boolean",
134
- name: "one-role-only",
135
- description: "Allow each member to hold only one role from this panel.",
136
- },
137
- {
138
- type: "boolean",
139
- name: "persistent-reactions",
140
- description: "Keep member reactions visible and synchronised with roles.",
141
- },
142
- ],
143
- rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
144
- execute: startCreate,
145
- },
146
- {
147
- name: "edit",
148
- acknowledgement: "immediate-modal",
149
- description: "Edit an existing role panel.",
150
- permissionId: "reactionroles.manage",
151
- options: [
152
- referenceOption(),
153
- {
154
- type: "boolean",
155
- name: "persistent-reactions",
156
- description: "Keep member reactions visible and synchronised with roles.",
157
- },
158
- ],
159
- autocomplete: autocompletePanelReference,
160
- execute: startEdit,
161
- },
162
- {
163
- name: "list",
164
- acknowledgement: "deferred-ephemeral",
165
- description: "List the active and degraded role panels.",
166
- permissionId: "reactionroles.manage",
167
- execute: listPanels,
168
- },
169
- {
170
- name: "repair",
171
- acknowledgement: "deferred-ephemeral",
172
- description: "Revalidate and repair a role panel.",
173
- permissionId: "reactionroles.manage",
174
- options: [referenceOption()],
175
- autocomplete: autocompletePanelReference,
176
- execute: repairPanel,
177
- },
178
- {
179
- name: "delete",
180
- acknowledgement: "deferred-ephemeral",
181
- description: "Delete a role panel without changing member roles.",
182
- permissionId: "reactionroles.delete",
183
- options: [referenceOption()],
184
- autocomplete: autocompletePanelReference,
185
- execute: prepareDelete,
186
- },
187
- ],
188
- },
189
- ],
190
93
  buttons: [
191
94
  {
192
95
  customId: COMPONENT_IDS.continueDraft,
@@ -432,43 +335,6 @@ export const reactionRolesModule = defineModule({
432
335
  },
433
336
  health: () => ({ status: healthStatus, message: healthMessage }),
434
337
  });
435
- async function startCreate(context) {
436
- const guildId = requireGuild(context);
437
- const channel = context.getChannelOption("channel");
438
- const style = context.getStringOption("style");
439
- const control = context.getStringOption("control");
440
- const sessions = sessionService(context.services);
441
- const nextState = {
442
- mode: "create",
443
- selectionMode: context.getBooleanOption("one-role-only") === true
444
- ? "one_only"
445
- : "multiple",
446
- persistentReactions: context.getBooleanOption("persistent-reactions") !== false,
447
- ...(channel ? { channelId: channel.id } : {}),
448
- ...(style ? { messageStyle: requiredStyle([style]) } : {}),
449
- ...(control ? { controlType: requiredControl([control]) } : {}),
450
- };
451
- const existing = await sessions.getFor(guildId, context.userId, PANEL_PURPOSE);
452
- if (existing) {
453
- await sessions.update({
454
- sessionId: existing.sessionId,
455
- state: { ...existing.state, pendingReplacement: nextState },
456
- expectedVersion: existing.version,
457
- expiresAt: expiresAt(),
458
- });
459
- await context.reply(existingDraftResponse());
460
- return;
461
- }
462
- await sessions.create({
463
- guildId,
464
- userId: context.userId,
465
- purpose: PANEL_PURPOSE,
466
- state: nextState,
467
- expiresAt: expiresAt(),
468
- replace: true,
469
- });
470
- await context.showModal(basicModal(defaultsFromSessionState(nextState)));
471
- }
472
338
  async function continueDraft(context) {
473
339
  const guildId = requireGuild(context);
474
340
  const session = await sessionService(context.services).getFor(guildId, context.userId, PANEL_PURPOSE);
@@ -525,108 +391,6 @@ async function keepDraft(context) {
525
391
  },
526
392
  });
527
393
  }
528
- async function startEdit(context) {
529
- const guildId = requireGuild(context);
530
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
531
- const persistentReactions = context.getBooleanOption("persistent-reactions");
532
- const draft = persistentReactions === null
533
- ? panel
534
- : validatePanelDraft({ ...panel, persistentReactions });
535
- await sessionService(context.services).create({
536
- guildId,
537
- userId: context.userId,
538
- purpose: PANEL_PURPOSE,
539
- state: {
540
- mode: "edit",
541
- panelId: panel.panelId,
542
- expectedRevision: panel.revision,
543
- draft: serializeDraft(draft),
544
- },
545
- expiresAt: expiresAt(),
546
- replace: true,
547
- });
548
- await context.showModal(basicModal(draft, true));
549
- }
550
- async function listPanels(context) {
551
- const guildId = requireGuild(context);
552
- const result = await panels.repository(context.services).list({
553
- guildId,
554
- limit: 10,
555
- });
556
- await context.reply({
557
- ephemeral: true,
558
- componentsV2: {
559
- text: [
560
- {
561
- content: result.panels.length === 0
562
- ? "No Reaction Roles panels have been created."
563
- : result.panels
564
- .map((panel) => `**${panel.title}** · \`${panel.panelId}\` · ${panel.status} · revision ${panel.revision}`)
565
- .join("\n"),
566
- },
567
- ],
568
- },
569
- allowedUserMentionIds: [],
570
- });
571
- }
572
- async function repairPanel(context) {
573
- const guildId = requireGuild(context);
574
- await context.deferReply?.(true);
575
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
576
- const repaired = await panels.repair(interactionMutationContext(context, guildId), panel.panelId, panel.revision, false);
577
- await context.reply({
578
- ephemeral: true,
579
- componentsV2: {
580
- text: [
581
- {
582
- content: `Role panel **${repaired.title}** is active at revision ${repaired.revision}.`,
583
- },
584
- ],
585
- },
586
- allowedUserMentionIds: [],
587
- });
588
- }
589
- async function prepareDelete(context) {
590
- const guildId = requireGuild(context);
591
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
592
- await sessionService(context.services).create({
593
- guildId,
594
- userId: context.userId,
595
- purpose: PANEL_PURPOSE,
596
- state: {
597
- mode: "delete",
598
- panelId: panel.panelId,
599
- expectedRevision: panel.revision,
600
- },
601
- expiresAt: expiresAt(),
602
- replace: true,
603
- });
604
- await context.reply({
605
- ephemeral: true,
606
- componentsV2: {
607
- text: [
608
- {
609
- content: `Delete **${panel.title}**? Helyx will try to remove its panel message. Existing member roles will not change.`,
610
- },
611
- ],
612
- },
613
- buttons: [
614
- {
615
- type: "button",
616
- customId: COMPONENT_IDS.deleteConfirm,
617
- label: "Delete role panel",
618
- style: "danger",
619
- },
620
- {
621
- type: "button",
622
- customId: COMPONENT_IDS.cancel,
623
- label: "Keep role panel",
624
- style: "secondary",
625
- },
626
- ],
627
- allowedUserMentionIds: [],
628
- });
629
- }
630
394
  async function saveBasicDraft(context) {
631
395
  const guildId = requireGuild(context);
632
396
  const sessions = sessionService(context.services);
@@ -1204,39 +968,6 @@ function basicModal(panel, editing = false) {
1204
968
  ],
1205
969
  };
1206
970
  }
1207
- function existingDraftResponse() {
1208
- return {
1209
- ephemeral: true,
1210
- componentsV2: {
1211
- text: [
1212
- {
1213
- content: "You already have a Reaction Roles draft. Continue it or replace it with a new draft.",
1214
- },
1215
- ],
1216
- },
1217
- buttons: [
1218
- {
1219
- type: "button",
1220
- customId: COMPONENT_IDS.continueDraft,
1221
- label: "Continue draft",
1222
- style: "primary",
1223
- },
1224
- {
1225
- type: "button",
1226
- customId: COMPONENT_IDS.replaceDraft,
1227
- label: "Replace draft",
1228
- style: "danger",
1229
- },
1230
- {
1231
- type: "button",
1232
- customId: COMPONENT_IDS.keepDraft,
1233
- label: "Cancel",
1234
- style: "secondary",
1235
- },
1236
- ],
1237
- allowedUserMentionIds: [],
1238
- };
1239
- }
1240
971
  function defaultsFromSessionState(state) {
1241
972
  return {
1242
973
  ...(typeof state.channelId === "string"
@@ -1402,62 +1133,6 @@ function interactionMutationContext(context, guildId) {
1402
1133
  addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => context.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
1403
1134
  };
1404
1135
  }
1405
- async function findPanelReference(services, guildId, value) {
1406
- const reference = value?.trim();
1407
- if (!reference)
1408
- throw new Error("Role panel reference is required");
1409
- const panel = await panels.repository(services).findById(guildId, reference);
1410
- if (!panel)
1411
- throw new Error("Role panel was not found");
1412
- return panel;
1413
- }
1414
- async function autocompletePanelReference(context) {
1415
- if (!context.serverId || context.focusedOptionName !== "reference") {
1416
- await context.respond([]);
1417
- return;
1418
- }
1419
- const query = context.focusedValue.trim().normalize("NFKC").toLowerCase();
1420
- const repository = panels.repository(context.services);
1421
- const choices = [];
1422
- let cursor;
1423
- for (let pageNumber = 0; pageNumber < 5 && choices.length < 25; pageNumber += 1) {
1424
- const page = await repository.listReferences({
1425
- guildId: context.serverId,
1426
- ...(cursor ? { cursor } : {}),
1427
- limit: query ? 100 : 25,
1428
- });
1429
- for (const panel of page.panels) {
1430
- if (panel.status === "deleting")
1431
- continue;
1432
- const searchable = `${panel.title} ${panel.panelId} ${panel.status}`.toLowerCase();
1433
- if (query && !searchable.includes(query))
1434
- continue;
1435
- const title = panel.title.replaceAll(/\s+/gu, " ").trim();
1436
- const suffix = `${panel.status} · ${panel.panelId.slice(0, 8)}`;
1437
- choices.push({
1438
- name: `${title.slice(0, Math.max(1, 97 - suffix.length))} · ${suffix}`,
1439
- value: panel.panelId,
1440
- });
1441
- if (choices.length === 25)
1442
- break;
1443
- }
1444
- if (!query || !page.nextCursor)
1445
- break;
1446
- cursor = page.nextCursor;
1447
- }
1448
- await context.respond(choices);
1449
- }
1450
- function referenceOption() {
1451
- return {
1452
- type: "string",
1453
- name: "reference",
1454
- description: "Panel reference shown by /reaction-role list.",
1455
- required: true,
1456
- minLength: 1,
1457
- maxLength: 100,
1458
- autocomplete: true,
1459
- };
1460
- }
1461
1136
  function sessionService(services) {
1462
1137
  return services.get(HELYX_SERVICE_NAMES.sessions);
1463
1138
  }
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "releaseStatus": "live",
4
4
  "id": "helyx.reactionroles",
5
5
  "name": "Reaction Roles",
6
- "version": "1.0.12",
6
+ "version": "1.0.14",
7
7
  "description": "Let members select approved roles themselves.",
8
8
  "entry": "./dist/index.js",
9
9
  "framework": "^0.1.0",
@@ -307,7 +307,7 @@
307
307
  }
308
308
  },
309
309
  "contributions": {
310
- "commands": ["reaction-role"],
310
+ "commands": [],
311
311
  "events": [
312
312
  "discord.channel-delete",
313
313
  "discord.channel-update",
@@ -1,3 +1,5 @@
1
+ -- Baseline source: 0001_reactionroles_foundation.sql
2
+
1
3
  CREATE TABLE IF NOT EXISTS reactionroles_panels (
2
4
  panel_id text PRIMARY KEY,
3
5
  guild_id text NOT NULL,
@@ -106,3 +108,61 @@ CREATE UNIQUE INDEX IF NOT EXISTS reactionroles_cleanup_message_reason_idx
106
108
  message_id,
107
109
  reason
108
110
  );
111
+
112
+ -- Baseline source: 0002_cleanup_retry_order.sql
113
+
114
+ UPDATE reactionroles_message_cleanup
115
+ SET next_attempt_at = created_at
116
+ WHERE next_attempt_at IS NULL;
117
+
118
+ ALTER TABLE reactionroles_message_cleanup
119
+ ALTER COLUMN next_attempt_at SET NOT NULL;
120
+
121
+ -- Baseline source: 0003_persistent_reactions.sql
122
+
123
+ ALTER TABLE reactionroles_panels
124
+ ADD COLUMN IF NOT EXISTS persistent_reactions boolean NOT NULL DEFAULT true;
125
+
126
+ DO $$
127
+ DECLARE
128
+ constraint_name text;
129
+ BEGIN
130
+ FOR constraint_name IN
131
+ SELECT conname
132
+ FROM pg_constraint
133
+ WHERE conrelid = 'reactionroles_panels'::regclass
134
+ AND contype = 'c'
135
+ AND conname <> 'reactionroles_panels_reaction_selection_check'
136
+ AND pg_get_constraintdef(oid) LIKE '%control_type%reactions%selection_mode%'
137
+ LOOP
138
+ EXECUTE format(
139
+ 'ALTER TABLE reactionroles_panels DROP CONSTRAINT %I',
140
+ constraint_name
141
+ );
142
+ END LOOP;
143
+
144
+ IF NOT EXISTS (
145
+ SELECT 1
146
+ FROM pg_constraint
147
+ WHERE conrelid = 'reactionroles_panels'::regclass
148
+ AND conname = 'reactionroles_panels_reaction_selection_check'
149
+ ) THEN
150
+ ALTER TABLE reactionroles_panels
151
+ ADD CONSTRAINT reactionroles_panels_reaction_selection_check
152
+ CHECK (
153
+ control_type <> 'reactions'
154
+ OR selection_mode <> 'one_only'
155
+ OR persistent_reactions = false
156
+ );
157
+ END IF;
158
+ END
159
+ $$;
160
+
161
+ -- Baseline source: 0004_entry_lookup_indexes.sql
162
+
163
+ CREATE INDEX IF NOT EXISTS reactionroles_entries_role_panel_idx
164
+ ON reactionroles_entries (role_id, panel_id);
165
+
166
+ CREATE INDEX IF NOT EXISTS reactionroles_entries_emoji_panel_idx
167
+ ON reactionroles_entries (emoji_id, panel_id)
168
+ WHERE emoji_id IS NOT NULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/module-reactionroles",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Configurable self-service Discord role panels for Helyx.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -50,6 +50,6 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@helyx/module-manifest": ">=0.7.0 <0.8.0",
53
- "@helyx/sdk": ">=0.8.0 <0.9.0"
53
+ "@helyx/sdk": ">=0.8.4 <0.9.0"
54
54
  }
55
55
  }
@@ -1,6 +0,0 @@
1
- UPDATE reactionroles_message_cleanup
2
- SET next_attempt_at = created_at
3
- WHERE next_attempt_at IS NULL;
4
-
5
- ALTER TABLE reactionroles_message_cleanup
6
- ALTER COLUMN next_attempt_at SET NOT NULL;
@@ -1,37 +0,0 @@
1
- ALTER TABLE reactionroles_panels
2
- ADD COLUMN IF NOT EXISTS persistent_reactions boolean NOT NULL DEFAULT true;
3
-
4
- DO $$
5
- DECLARE
6
- constraint_name text;
7
- BEGIN
8
- FOR constraint_name IN
9
- SELECT conname
10
- FROM pg_constraint
11
- WHERE conrelid = 'reactionroles_panels'::regclass
12
- AND contype = 'c'
13
- AND conname <> 'reactionroles_panels_reaction_selection_check'
14
- AND pg_get_constraintdef(oid) LIKE '%control_type%reactions%selection_mode%'
15
- LOOP
16
- EXECUTE format(
17
- 'ALTER TABLE reactionroles_panels DROP CONSTRAINT %I',
18
- constraint_name
19
- );
20
- END LOOP;
21
-
22
- IF NOT EXISTS (
23
- SELECT 1
24
- FROM pg_constraint
25
- WHERE conrelid = 'reactionroles_panels'::regclass
26
- AND conname = 'reactionroles_panels_reaction_selection_check'
27
- ) THEN
28
- ALTER TABLE reactionroles_panels
29
- ADD CONSTRAINT reactionroles_panels_reaction_selection_check
30
- CHECK (
31
- control_type <> 'reactions'
32
- OR selection_mode <> 'one_only'
33
- OR persistent_reactions = false
34
- );
35
- END IF;
36
- END
37
- $$;
@@ -1,6 +0,0 @@
1
- CREATE INDEX IF NOT EXISTS reactionroles_entries_role_panel_idx
2
- ON reactionroles_entries (role_id, panel_id);
3
-
4
- CREATE INDEX IF NOT EXISTS reactionroles_entries_emoji_panel_idx
5
- ON reactionroles_entries (emoji_id, panel_id)
6
- WHERE emoji_id IS NOT NULL;