@helyx/module-reactionroles 1.0.11 → 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,25 @@
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
+
15
+ ## 1.0.12
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [26b4c3e]
20
+ - @helyx/module-manifest@0.7.0
21
+ - @helyx/sdk@0.8.0
22
+
3
23
  ## 1.0.11
4
24
 
5
25
  ### Patch Changes
package/dist/index.js CHANGED
@@ -90,98 +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
- description: "Open the guided Reaction Roles builder.",
104
- permissionId: "reactionroles.create",
105
- options: [
106
- {
107
- type: "channel",
108
- name: "channel",
109
- description: "Prefill the destination channel.",
110
- channelTypes: ["guild_text", "guild_announcement"],
111
- },
112
- {
113
- type: "string",
114
- name: "style",
115
- description: "Prefill the public message style.",
116
- choices: [
117
- { name: "Text", value: "text" },
118
- { name: "Embed", value: "embed" },
119
- ],
120
- },
121
- {
122
- type: "string",
123
- name: "control",
124
- description: "Prefill how members choose roles.",
125
- choices: [
126
- { name: "Buttons", value: "buttons" },
127
- { name: "Dropdown", value: "dropdown" },
128
- { name: "Emoji reactions", value: "reactions" },
129
- ],
130
- },
131
- {
132
- type: "boolean",
133
- name: "one-role-only",
134
- description: "Allow each member to hold only one role from this panel.",
135
- },
136
- {
137
- type: "boolean",
138
- name: "persistent-reactions",
139
- description: "Keep member reactions visible and synchronised with roles.",
140
- },
141
- ],
142
- rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
143
- execute: startCreate,
144
- },
145
- {
146
- name: "edit",
147
- description: "Edit an existing role panel.",
148
- permissionId: "reactionroles.manage",
149
- options: [
150
- referenceOption(),
151
- {
152
- type: "boolean",
153
- name: "persistent-reactions",
154
- description: "Keep member reactions visible and synchronised with roles.",
155
- },
156
- ],
157
- autocomplete: autocompletePanelReference,
158
- execute: startEdit,
159
- },
160
- {
161
- name: "list",
162
- description: "List the active and degraded role panels.",
163
- permissionId: "reactionroles.manage",
164
- execute: listPanels,
165
- },
166
- {
167
- name: "repair",
168
- description: "Revalidate and repair a role panel.",
169
- permissionId: "reactionroles.manage",
170
- options: [referenceOption()],
171
- autocomplete: autocompletePanelReference,
172
- execute: repairPanel,
173
- },
174
- {
175
- name: "delete",
176
- description: "Delete a role panel without changing member roles.",
177
- permissionId: "reactionroles.delete",
178
- options: [referenceOption()],
179
- autocomplete: autocompletePanelReference,
180
- execute: prepareDelete,
181
- },
182
- ],
183
- },
184
- ],
185
93
  buttons: [
186
94
  {
187
95
  customId: COMPONENT_IDS.continueDraft,
@@ -427,43 +335,6 @@ export const reactionRolesModule = defineModule({
427
335
  },
428
336
  health: () => ({ status: healthStatus, message: healthMessage }),
429
337
  });
430
- async function startCreate(context) {
431
- const guildId = requireGuild(context);
432
- const channel = context.getChannelOption("channel");
433
- const style = context.getStringOption("style");
434
- const control = context.getStringOption("control");
435
- const sessions = sessionService(context.services);
436
- const nextState = {
437
- mode: "create",
438
- selectionMode: context.getBooleanOption("one-role-only") === true
439
- ? "one_only"
440
- : "multiple",
441
- persistentReactions: context.getBooleanOption("persistent-reactions") !== false,
442
- ...(channel ? { channelId: channel.id } : {}),
443
- ...(style ? { messageStyle: requiredStyle([style]) } : {}),
444
- ...(control ? { controlType: requiredControl([control]) } : {}),
445
- };
446
- const existing = await sessions.getFor(guildId, context.userId, PANEL_PURPOSE);
447
- if (existing) {
448
- await sessions.update({
449
- sessionId: existing.sessionId,
450
- state: { ...existing.state, pendingReplacement: nextState },
451
- expectedVersion: existing.version,
452
- expiresAt: expiresAt(),
453
- });
454
- await context.reply(existingDraftResponse());
455
- return;
456
- }
457
- await sessions.create({
458
- guildId,
459
- userId: context.userId,
460
- purpose: PANEL_PURPOSE,
461
- state: nextState,
462
- expiresAt: expiresAt(),
463
- replace: true,
464
- });
465
- await context.showModal(basicModal(defaultsFromSessionState(nextState)));
466
- }
467
338
  async function continueDraft(context) {
468
339
  const guildId = requireGuild(context);
469
340
  const session = await sessionService(context.services).getFor(guildId, context.userId, PANEL_PURPOSE);
@@ -520,108 +391,6 @@ async function keepDraft(context) {
520
391
  },
521
392
  });
522
393
  }
523
- async function startEdit(context) {
524
- const guildId = requireGuild(context);
525
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
526
- const persistentReactions = context.getBooleanOption("persistent-reactions");
527
- const draft = persistentReactions === null
528
- ? panel
529
- : validatePanelDraft({ ...panel, persistentReactions });
530
- await sessionService(context.services).create({
531
- guildId,
532
- userId: context.userId,
533
- purpose: PANEL_PURPOSE,
534
- state: {
535
- mode: "edit",
536
- panelId: panel.panelId,
537
- expectedRevision: panel.revision,
538
- draft: serializeDraft(draft),
539
- },
540
- expiresAt: expiresAt(),
541
- replace: true,
542
- });
543
- await context.showModal(basicModal(draft, true));
544
- }
545
- async function listPanels(context) {
546
- const guildId = requireGuild(context);
547
- const result = await panels.repository(context.services).list({
548
- guildId,
549
- limit: 10,
550
- });
551
- await context.reply({
552
- ephemeral: true,
553
- componentsV2: {
554
- text: [
555
- {
556
- content: result.panels.length === 0
557
- ? "No Reaction Roles panels have been created."
558
- : result.panels
559
- .map((panel) => `**${panel.title}** · \`${panel.panelId}\` · ${panel.status} · revision ${panel.revision}`)
560
- .join("\n"),
561
- },
562
- ],
563
- },
564
- allowedUserMentionIds: [],
565
- });
566
- }
567
- async function repairPanel(context) {
568
- const guildId = requireGuild(context);
569
- await context.deferReply?.(true);
570
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
571
- const repaired = await panels.repair(interactionMutationContext(context, guildId), panel.panelId, panel.revision, false);
572
- await context.reply({
573
- ephemeral: true,
574
- componentsV2: {
575
- text: [
576
- {
577
- content: `Role panel **${repaired.title}** is active at revision ${repaired.revision}.`,
578
- },
579
- ],
580
- },
581
- allowedUserMentionIds: [],
582
- });
583
- }
584
- async function prepareDelete(context) {
585
- const guildId = requireGuild(context);
586
- const panel = await findPanelReference(context.services, guildId, context.getStringOption("reference"));
587
- await sessionService(context.services).create({
588
- guildId,
589
- userId: context.userId,
590
- purpose: PANEL_PURPOSE,
591
- state: {
592
- mode: "delete",
593
- panelId: panel.panelId,
594
- expectedRevision: panel.revision,
595
- },
596
- expiresAt: expiresAt(),
597
- replace: true,
598
- });
599
- await context.reply({
600
- ephemeral: true,
601
- componentsV2: {
602
- text: [
603
- {
604
- content: `Delete **${panel.title}**? Helyx will try to remove its panel message. Existing member roles will not change.`,
605
- },
606
- ],
607
- },
608
- buttons: [
609
- {
610
- type: "button",
611
- customId: COMPONENT_IDS.deleteConfirm,
612
- label: "Delete role panel",
613
- style: "danger",
614
- },
615
- {
616
- type: "button",
617
- customId: COMPONENT_IDS.cancel,
618
- label: "Keep role panel",
619
- style: "secondary",
620
- },
621
- ],
622
- allowedUserMentionIds: [],
623
- });
624
- }
625
394
  async function saveBasicDraft(context) {
626
395
  const guildId = requireGuild(context);
627
396
  const sessions = sessionService(context.services);
@@ -1199,39 +968,6 @@ function basicModal(panel, editing = false) {
1199
968
  ],
1200
969
  };
1201
970
  }
1202
- function existingDraftResponse() {
1203
- return {
1204
- ephemeral: true,
1205
- componentsV2: {
1206
- text: [
1207
- {
1208
- content: "You already have a Reaction Roles draft. Continue it or replace it with a new draft.",
1209
- },
1210
- ],
1211
- },
1212
- buttons: [
1213
- {
1214
- type: "button",
1215
- customId: COMPONENT_IDS.continueDraft,
1216
- label: "Continue draft",
1217
- style: "primary",
1218
- },
1219
- {
1220
- type: "button",
1221
- customId: COMPONENT_IDS.replaceDraft,
1222
- label: "Replace draft",
1223
- style: "danger",
1224
- },
1225
- {
1226
- type: "button",
1227
- customId: COMPONENT_IDS.keepDraft,
1228
- label: "Cancel",
1229
- style: "secondary",
1230
- },
1231
- ],
1232
- allowedUserMentionIds: [],
1233
- };
1234
- }
1235
971
  function defaultsFromSessionState(state) {
1236
972
  return {
1237
973
  ...(typeof state.channelId === "string"
@@ -1397,62 +1133,6 @@ function interactionMutationContext(context, guildId) {
1397
1133
  addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => context.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
1398
1134
  };
1399
1135
  }
1400
- async function findPanelReference(services, guildId, value) {
1401
- const reference = value?.trim();
1402
- if (!reference)
1403
- throw new Error("Role panel reference is required");
1404
- const panel = await panels.repository(services).findById(guildId, reference);
1405
- if (!panel)
1406
- throw new Error("Role panel was not found");
1407
- return panel;
1408
- }
1409
- async function autocompletePanelReference(context) {
1410
- if (!context.serverId || context.focusedOptionName !== "reference") {
1411
- await context.respond([]);
1412
- return;
1413
- }
1414
- const query = context.focusedValue.trim().normalize("NFKC").toLowerCase();
1415
- const repository = panels.repository(context.services);
1416
- const choices = [];
1417
- let cursor;
1418
- for (let pageNumber = 0; pageNumber < 5 && choices.length < 25; pageNumber += 1) {
1419
- const page = await repository.listReferences({
1420
- guildId: context.serverId,
1421
- ...(cursor ? { cursor } : {}),
1422
- limit: query ? 100 : 25,
1423
- });
1424
- for (const panel of page.panels) {
1425
- if (panel.status === "deleting")
1426
- continue;
1427
- const searchable = `${panel.title} ${panel.panelId} ${panel.status}`.toLowerCase();
1428
- if (query && !searchable.includes(query))
1429
- continue;
1430
- const title = panel.title.replaceAll(/\s+/gu, " ").trim();
1431
- const suffix = `${panel.status} · ${panel.panelId.slice(0, 8)}`;
1432
- choices.push({
1433
- name: `${title.slice(0, Math.max(1, 97 - suffix.length))} · ${suffix}`,
1434
- value: panel.panelId,
1435
- });
1436
- if (choices.length === 25)
1437
- break;
1438
- }
1439
- if (!query || !page.nextCursor)
1440
- break;
1441
- cursor = page.nextCursor;
1442
- }
1443
- await context.respond(choices);
1444
- }
1445
- function referenceOption() {
1446
- return {
1447
- type: "string",
1448
- name: "reference",
1449
- description: "Panel reference shown by /reaction-role list.",
1450
- required: true,
1451
- minLength: 1,
1452
- maxLength: 100,
1453
- autocomplete: true,
1454
- };
1455
- }
1456
1136
  function sessionService(services) {
1457
1137
  return services.get(HELYX_SERVICE_NAMES.sessions);
1458
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.11",
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",
@@ -283,10 +283,31 @@
283
283
  }
284
284
  ]
285
285
  }
286
- ]
286
+ ],
287
+ "workspace": {
288
+ "type": "managed_resources_v1",
289
+ "settings": [
290
+ {
291
+ "resources": ["panels"],
292
+ "mode": "create-only"
293
+ }
294
+ ],
295
+ "tabs": [
296
+ {
297
+ "id": "panels",
298
+ "label": "Panels",
299
+ "panels": [
300
+ {
301
+ "resources": ["panels"],
302
+ "mode": "list-only"
303
+ }
304
+ ]
305
+ }
306
+ ]
307
+ }
287
308
  },
288
309
  "contributions": {
289
- "commands": ["reaction-role"],
310
+ "commands": [],
290
311
  "events": [
291
312
  "discord.channel-delete",
292
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.11",
3
+ "version": "1.0.14",
4
4
  "description": "Configurable self-service Discord role panels for Helyx.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -49,7 +49,7 @@
49
49
  "build": "tsc -b"
50
50
  },
51
51
  "dependencies": {
52
- "@helyx/module-manifest": ">=0.6.0 <0.7.0",
53
- "@helyx/sdk": ">=0.7.0 <0.8.0"
52
+ "@helyx/module-manifest": ">=0.7.0 <0.8.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;