@helyx/module-reactionroles 1.0.1

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/manifest.json ADDED
@@ -0,0 +1,310 @@
1
+ {
2
+ "manifestVersion": "0.2",
3
+ "releaseStatus": "live",
4
+ "id": "helyx.reactionroles",
5
+ "name": "Reaction Roles",
6
+ "version": "1.0.1",
7
+ "description": "Let members select approved roles themselves.",
8
+ "entry": "./dist/index.js",
9
+ "framework": "^0.1.0",
10
+ "dependencies": [],
11
+ "permissions": [
12
+ "ADD_REACTIONS",
13
+ "EMBED_LINKS",
14
+ "MANAGE_MESSAGES",
15
+ "MANAGE_ROLES",
16
+ "READ_MESSAGE_HISTORY",
17
+ "SEND_MESSAGES",
18
+ "VIEW_CHANNEL"
19
+ ],
20
+ "privacy": {
21
+ "containsPersonalData": true,
22
+ "providerVersion": 1,
23
+ "collections": [
24
+ {
25
+ "name": "panels",
26
+ "createdAtField": "created_at",
27
+ "subjectFields": [
28
+ {
29
+ "field": "created_by_user_id",
30
+ "role": "creator",
31
+ "erasure": "set-null"
32
+ },
33
+ {
34
+ "field": "updated_by_user_id",
35
+ "role": "updater",
36
+ "erasure": "set-null"
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ "dashboard": {
43
+ "category": "automation",
44
+ "icon": "roles",
45
+ "settings": [],
46
+ "resources": [
47
+ {
48
+ "id": "panels",
49
+ "name": "Role panels",
50
+ "singularName": "Role panel",
51
+ "description": "Create and manage Helyx-owned self-service role panels.",
52
+ "publicationMode": "validate_on_publish",
53
+ "fields": [
54
+ {
55
+ "key": "channelId",
56
+ "type": "discord-channel",
57
+ "label": "Destination channel",
58
+ "description": "Text or announcement channel that receives the panel.",
59
+ "required": true,
60
+ "group": "Panel setup",
61
+ "channelTypes": ["guild_text", "guild_announcement"]
62
+ },
63
+ {
64
+ "key": "title",
65
+ "type": "string",
66
+ "label": "Title",
67
+ "description": "Short public panel title.",
68
+ "required": true,
69
+ "group": "Message",
70
+ "minLength": 1,
71
+ "maxLength": 80
72
+ },
73
+ {
74
+ "key": "messageBody",
75
+ "type": "string",
76
+ "label": "Message",
77
+ "description": "Public guidance shown above the role controls.",
78
+ "required": true,
79
+ "group": "Message",
80
+ "multiline": true,
81
+ "minLength": 1,
82
+ "maxLength": 1000
83
+ },
84
+ {
85
+ "key": "messageStyle",
86
+ "type": "select",
87
+ "label": "Message style",
88
+ "description": "Show the panel as clear text or a compact embed.",
89
+ "required": true,
90
+ "group": "Message",
91
+ "options": [
92
+ {
93
+ "value": "text",
94
+ "label": "Text"
95
+ },
96
+ {
97
+ "value": "embed",
98
+ "label": "Embed"
99
+ }
100
+ ],
101
+ "default": "text"
102
+ },
103
+ {
104
+ "key": "controlType",
105
+ "type": "select",
106
+ "label": "Role controls",
107
+ "description": "Choose buttons, a dropdown or emoji reactions.",
108
+ "required": true,
109
+ "group": "Panel setup",
110
+ "options": [
111
+ {
112
+ "value": "buttons",
113
+ "label": "Buttons"
114
+ },
115
+ {
116
+ "value": "dropdown",
117
+ "label": "Dropdown"
118
+ },
119
+ {
120
+ "value": "reactions",
121
+ "label": "Emoji reactions"
122
+ }
123
+ ],
124
+ "default": "buttons"
125
+ },
126
+ {
127
+ "key": "selectionMode",
128
+ "type": "select",
129
+ "label": "Selection mode",
130
+ "description": "Allow several panel roles or only one.",
131
+ "required": true,
132
+ "group": "Panel setup",
133
+ "options": [
134
+ {
135
+ "value": "multiple",
136
+ "label": "Several roles"
137
+ },
138
+ {
139
+ "value": "one_only",
140
+ "label": "One role only"
141
+ }
142
+ ],
143
+ "default": "multiple"
144
+ },
145
+ {
146
+ "key": "persistentReactions",
147
+ "type": "boolean",
148
+ "label": "Persistent reactions",
149
+ "description": "Keep member reactions visible and remove roles when members unreact. Turn this off to remove each member reaction after assigning its role.",
150
+ "required": true,
151
+ "group": "Panel setup",
152
+ "visibleWhen": {
153
+ "field": "controlType",
154
+ "equals": "reactions"
155
+ },
156
+ "default": true
157
+ },
158
+ {
159
+ "key": "embedColour",
160
+ "type": "integer",
161
+ "label": "Embed colour",
162
+ "description": "Discord embed colour as a six-digit hex code.",
163
+ "required": false,
164
+ "group": "Message",
165
+ "visibleWhen": {
166
+ "field": "messageStyle",
167
+ "equals": "embed"
168
+ },
169
+ "minimum": 0,
170
+ "maximum": 16777215,
171
+ "default": 8141549,
172
+ "display": "hex_colour"
173
+ },
174
+ {
175
+ "key": "dropdownPlaceholder",
176
+ "type": "string",
177
+ "label": "Dropdown label",
178
+ "description": "Short instruction shown before a member opens the dropdown.",
179
+ "required": false,
180
+ "group": "Panel setup",
181
+ "visibleWhen": {
182
+ "field": "controlType",
183
+ "equals": "dropdown"
184
+ },
185
+ "minLength": 1,
186
+ "maxLength": 100,
187
+ "default": "Choose your roles"
188
+ },
189
+ {
190
+ "key": "entries",
191
+ "type": "list",
192
+ "label": "Role entries",
193
+ "description": "Roles and labels available from this panel.",
194
+ "required": true,
195
+ "group": "Roles",
196
+ "minItems": 1,
197
+ "maxItems": 25,
198
+ "itemLabel": "Role entry",
199
+ "fields": [
200
+ {
201
+ "key": "roleId",
202
+ "type": "discord-role",
203
+ "label": "Role",
204
+ "description": "Approved role members can add or remove.",
205
+ "required": true
206
+ },
207
+ {
208
+ "key": "nameplate",
209
+ "type": "string",
210
+ "label": "Label",
211
+ "description": "Public label for this role.",
212
+ "required": true,
213
+ "minLength": 1,
214
+ "maxLength": 80
215
+ },
216
+ {
217
+ "key": "emoji",
218
+ "type": "string",
219
+ "label": "Emoji",
220
+ "description": "Optional for buttons or dropdowns and required for reactions.",
221
+ "required": false,
222
+ "minLength": 1,
223
+ "maxLength": 100,
224
+ "placeholder": "✅ or <:updates:123456789012345678>",
225
+ "display": "emoji"
226
+ },
227
+ {
228
+ "key": "buttonStyle",
229
+ "type": "select",
230
+ "label": "Button colour",
231
+ "description": "Colour used for a button control.",
232
+ "required": false,
233
+ "options": [
234
+ {
235
+ "value": "primary",
236
+ "label": "Blue"
237
+ },
238
+ {
239
+ "value": "secondary",
240
+ "label": "Grey"
241
+ },
242
+ {
243
+ "value": "success",
244
+ "label": "Green"
245
+ },
246
+ {
247
+ "value": "danger",
248
+ "label": "Red"
249
+ }
250
+ ],
251
+ "default": "secondary"
252
+ }
253
+ ]
254
+ }
255
+ ],
256
+ "actions": [
257
+ {
258
+ "id": "duplicate",
259
+ "label": "Duplicate panel",
260
+ "description": "Create and publish a new panel using these settings.",
261
+ "style": "secondary",
262
+ "confirmation": "Publish a duplicate of this role panel?"
263
+ },
264
+ {
265
+ "id": "repair",
266
+ "label": "Repair panel",
267
+ "description": "Revalidate the panel and recreate its Helyx-owned message when required.",
268
+ "style": "secondary"
269
+ },
270
+ {
271
+ "id": "repost",
272
+ "label": "Repost panel",
273
+ "description": "Publish a replacement Helyx-owned message and retire the old reference.",
274
+ "style": "secondary",
275
+ "confirmation": "Repost this role panel and replace its current Discord message?"
276
+ },
277
+ {
278
+ "id": "delete",
279
+ "label": "Delete panel",
280
+ "description": "Delete Helyx data and attempt to remove its current panel message.",
281
+ "style": "danger",
282
+ "confirmation": "Delete this role panel? Existing member roles will not be changed."
283
+ }
284
+ ]
285
+ }
286
+ ]
287
+ },
288
+ "contributions": {
289
+ "commands": ["reaction-role"],
290
+ "events": [
291
+ "discord.channel-delete",
292
+ "discord.channel-update",
293
+ "discord.guild-emoji-delete",
294
+ "discord.guild-emoji-update",
295
+ "discord.guild-role-delete",
296
+ "discord.guild-role-update",
297
+ "discord.message-delete",
298
+ "discord.message-reaction-add",
299
+ "discord.message-reaction-remove"
300
+ ],
301
+ "logs": [
302
+ "reaction-role-panel-created",
303
+ "reaction-role-panel-deleted",
304
+ "reaction-role-panel-updated",
305
+ "reaction-role-panel-reposted",
306
+ "reaction-role-panel-repaired",
307
+ "reaction-role-member-changed"
308
+ ]
309
+ }
310
+ }
@@ -0,0 +1,108 @@
1
+ CREATE TABLE IF NOT EXISTS reactionroles_panels (
2
+ panel_id text PRIMARY KEY,
3
+ guild_id text NOT NULL,
4
+ channel_id text NOT NULL,
5
+ message_id text,
6
+ title text NOT NULL CHECK (length(title) BETWEEN 1 AND 80),
7
+ message_body text NOT NULL CHECK (length(message_body) BETWEEN 1 AND 1000),
8
+ message_style text NOT NULL CHECK (message_style IN ('text', 'embed')),
9
+ control_type text NOT NULL CHECK (
10
+ control_type IN ('buttons', 'dropdown', 'reactions')
11
+ ),
12
+ selection_mode text NOT NULL CHECK (
13
+ selection_mode IN ('multiple', 'one_only')
14
+ ),
15
+ embed_colour integer CHECK (
16
+ embed_colour IS NULL OR embed_colour BETWEEN 0 AND 16777215
17
+ ),
18
+ dropdown_placeholder text CHECK (
19
+ dropdown_placeholder IS NULL
20
+ OR length(dropdown_placeholder) BETWEEN 1 AND 100
21
+ ),
22
+ status text NOT NULL CHECK (
23
+ status IN (
24
+ 'pending', 'active', 'disabled', 'message_missing',
25
+ 'needs_attention', 'cleanup_pending', 'deleting'
26
+ )
27
+ ),
28
+ revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
29
+ created_by_user_id text,
30
+ updated_by_user_id text,
31
+ created_at timestamptz NOT NULL,
32
+ updated_at timestamptz NOT NULL,
33
+ CHECK (
34
+ control_type <> 'reactions' OR selection_mode = 'multiple'
35
+ )
36
+ );
37
+
38
+ CREATE UNIQUE INDEX IF NOT EXISTS reactionroles_panels_message_idx
39
+ ON reactionroles_panels (guild_id, message_id)
40
+ WHERE message_id IS NOT NULL;
41
+
42
+ CREATE INDEX IF NOT EXISTS reactionroles_panels_guild_status_idx
43
+ ON reactionroles_panels (guild_id, status, updated_at DESC, panel_id);
44
+
45
+ CREATE TABLE IF NOT EXISTS reactionroles_entries (
46
+ entry_id text PRIMARY KEY,
47
+ panel_id text NOT NULL
48
+ REFERENCES reactionroles_panels (panel_id) ON DELETE CASCADE,
49
+ position integer NOT NULL CHECK (position BETWEEN 0 AND 24),
50
+ role_id text NOT NULL,
51
+ nameplate text NOT NULL CHECK (length(nameplate) BETWEEN 1 AND 80),
52
+ emoji_kind text CHECK (emoji_kind IS NULL OR emoji_kind IN ('unicode', 'custom')),
53
+ emoji_name text,
54
+ emoji_id text,
55
+ emoji_animated boolean,
56
+ button_style text CHECK (
57
+ button_style IS NULL
58
+ OR button_style IN ('primary', 'secondary', 'success', 'danger')
59
+ ),
60
+ created_at timestamptz NOT NULL,
61
+ updated_at timestamptz NOT NULL,
62
+ UNIQUE (panel_id, position),
63
+ UNIQUE (panel_id, role_id),
64
+ CHECK (
65
+ (emoji_kind IS NULL AND emoji_name IS NULL AND emoji_id IS NULL)
66
+ OR (emoji_kind = 'unicode' AND emoji_name IS NOT NULL AND emoji_id IS NULL)
67
+ OR (emoji_kind = 'custom' AND emoji_name IS NOT NULL AND emoji_id IS NOT NULL)
68
+ )
69
+ );
70
+
71
+ CREATE UNIQUE INDEX IF NOT EXISTS reactionroles_entries_emoji_idx
72
+ ON reactionroles_entries (
73
+ panel_id,
74
+ emoji_kind,
75
+ emoji_name,
76
+ COALESCE(emoji_id, '')
77
+ )
78
+ WHERE emoji_kind IS NOT NULL;
79
+
80
+ CREATE INDEX IF NOT EXISTS reactionroles_entries_panel_idx
81
+ ON reactionroles_entries (panel_id, position);
82
+
83
+ CREATE TABLE IF NOT EXISTS reactionroles_message_cleanup (
84
+ cleanup_id text PRIMARY KEY,
85
+ guild_id text NOT NULL,
86
+ panel_id text NOT NULL,
87
+ channel_id text NOT NULL,
88
+ message_id text NOT NULL,
89
+ reason text NOT NULL CHECK (
90
+ reason IN ('replacement', 'rollback', 'delete')
91
+ ),
92
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts BETWEEN 0 AND 10),
93
+ next_attempt_at timestamptz,
94
+ created_at timestamptz NOT NULL,
95
+ completed_at timestamptz
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS reactionroles_cleanup_pending_idx
99
+ ON reactionroles_message_cleanup (next_attempt_at, created_at)
100
+ WHERE completed_at IS NULL;
101
+
102
+ CREATE UNIQUE INDEX IF NOT EXISTS reactionroles_cleanup_message_reason_idx
103
+ ON reactionroles_message_cleanup (
104
+ guild_id,
105
+ channel_id,
106
+ message_id,
107
+ reason
108
+ );
@@ -0,0 +1,6 @@
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;
@@ -0,0 +1,37 @@
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
+ $$;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@helyx/module-reactionroles",
3
+ "version": "1.0.1",
4
+ "description": "Configurable self-service Discord role panels for Helyx.",
5
+ "keywords": [
6
+ "helyx",
7
+ "discord",
8
+ "roles",
9
+ "module"
10
+ ],
11
+ "private": false,
12
+ "license": "HSAL 1.0",
13
+ "homepage": "https://helyx.gg/modules/",
14
+ "bugs": {
15
+ "url": "https://github.com/ZyC0R3/Helyx/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ZyC0R3/Helyx.git",
20
+ "directory": "modules/reactionroles"
21
+ },
22
+ "engines": {
23
+ "node": ">=24 <27",
24
+ "npm": ">=11"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "type": "module",
31
+ "files": [
32
+ "dist",
33
+ "!dist/*.tsbuildinfo",
34
+ "!dist/**/*.map",
35
+ "migrations",
36
+ "manifest.json",
37
+ "README.md",
38
+ "CHANGELOG.md",
39
+ "LICENSE"
40
+ ],
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "default": "./dist/index.js"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -b"
50
+ },
51
+ "dependencies": {
52
+ "@helyx/module-manifest": "0.4.2",
53
+ "@helyx/sdk": "0.3.4"
54
+ }
55
+ }