Codex commited on
Commit
8960b50
·
1 Parent(s): 7695308

Add alerts role panel command

Browse files
Files changed (5) hide show
  1. src/commands.js +3 -0
  2. src/constants.js +11 -0
  3. src/embeds.js +51 -0
  4. src/index.js +106 -0
  5. test/alerts.test.js +43 -0
src/commands.js CHANGED
@@ -199,6 +199,9 @@ export const commands = [
199
  new SlashCommandBuilder()
200
  .setName('commands')
201
  .setDescription('Post a public command reference embed.'),
 
 
 
202
  new SlashCommandBuilder()
203
  .setName('welcome')
204
  .setDescription('Post the welcome embed and tag everyone.'),
 
199
  new SlashCommandBuilder()
200
  .setName('commands')
201
  .setDescription('Post a public command reference embed.'),
202
+ new SlashCommandBuilder()
203
+ .setName('alerts')
204
+ .setDescription('Post the analyst alert-role embed to the welcome channel.'),
205
  new SlashCommandBuilder()
206
  .setName('welcome')
207
  .setDescription('Post the welcome embed and tag everyone.'),
src/constants.js CHANGED
@@ -45,6 +45,17 @@ export const RESOLUTION_CHOICES = [
45
 
46
  export const BETS_PAGE_SIZE = 5;
47
 
 
 
 
 
 
 
 
 
 
 
 
48
  export const SPORT_CODES = {
49
  MLB: 'mlb',
50
  NBA: 'nba',
 
45
 
46
  export const BETS_PAGE_SIZE = 5;
47
 
48
+ export const ALERTS_ALLOWED_USERNAME = 'jew_olympics';
49
+ export const ALERTS_CHANNEL_ID = '1299315006298656840';
50
+ export const ALERT_ROLE_NAMES = [
51
+ 'PhazzAlerts',
52
+ 'KennyAlerts',
53
+ 'GarAlerts',
54
+ 'RIIPAlerts',
55
+ 'ZylisAlerts',
56
+ 'JRAlerts',
57
+ ];
58
+
59
  export const SPORT_CODES = {
60
  MLB: 'mlb',
61
  NBA: 'nba',
src/embeds.js CHANGED
@@ -1,5 +1,6 @@
1
  import { AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ActionRowBuilder } from 'discord.js';
2
  import { buildChartSummaryText, createProfitChartPng } from './chart.js';
 
3
 
4
  const PALETTE = {
5
  amber: 0xf59e0b,
@@ -288,6 +289,7 @@ export function buildCommandsEmbed() {
288
  { name: '/sports', value: 'Post public ROI and win-rate breakdowns by sport.' },
289
  { name: '/export', value: 'Export your filtered bets to CSV privately.' },
290
  { name: '/commands', value: 'Post this public command reference embed.' },
 
291
  { name: '/welcome', value: 'Post the public welcome embed and tag everyone. Only for Kenny F\'n Powers.' }
292
  );
293
  }
@@ -305,6 +307,42 @@ export function buildWelcomeEmbed() {
305
  .setFooter({ text: 'Built for fast manual logging inside Discord.' });
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  export function buildExportAttachment(rows) {
309
  const header = [
310
  'bet_number',
@@ -485,3 +523,16 @@ export function parseBetsPageId(customId) {
485
  status: status === '~' ? undefined : status,
486
  };
487
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ActionRowBuilder } from 'discord.js';
2
  import { buildChartSummaryText, createProfitChartPng } from './chart.js';
3
+ import { ALERT_ROLE_NAMES } from './constants.js';
4
 
5
  const PALETTE = {
6
  amber: 0xf59e0b,
 
289
  { name: '/sports', value: 'Post public ROI and win-rate breakdowns by sport.' },
290
  { name: '/export', value: 'Export your filtered bets to CSV privately.' },
291
  { name: '/commands', value: 'Post this public command reference embed.' },
292
+ { name: '/alerts', value: 'Post the public analyst alert-role panel to the welcome channel. Only for jew_olympics.' },
293
  { name: '/welcome', value: 'Post the public welcome embed and tag everyone. Only for Kenny F\'n Powers.' }
294
  );
295
  }
 
307
  .setFooter({ text: 'Built for fast manual logging inside Discord.' });
308
  }
309
 
310
+ export function buildAlertsEmbed() {
311
+ return new EmbedBuilder()
312
+ .setColor(PALETTE.gold)
313
+ .setTitle('Analyst Alert Roles')
314
+ .setDescription([
315
+ "We are moving from @ everyone alerts to individualized alerts. We have 6 analysts who will be sharing plays. You can have as many or as few alerts as you'd like. Click the button of the analyst(s) whose alerts you would like to receive.",
316
+ '',
317
+ '@PhazzAlerts - MLB / NBA',
318
+ '@KennyAlerts - MLB',
319
+ '@GarAlerts - MLB',
320
+ '@RIIPAlerts - MLB / NBA / NFL',
321
+ '@ZylisAlerts - MLB / NBA',
322
+ '@JRAlerts - NBA',
323
+ ].join('\n'))
324
+ .setFooter({ text: 'Click again any time to remove an alert role.' });
325
+ }
326
+
327
+ export function buildAlertsButtonRows() {
328
+ const rows = [];
329
+
330
+ for (let index = 0; index < ALERT_ROLE_NAMES.length; index += 3) {
331
+ rows.push(
332
+ new ActionRowBuilder().addComponents(
333
+ ...ALERT_ROLE_NAMES.slice(index, index + 3).map((roleName) =>
334
+ new ButtonBuilder()
335
+ .setCustomId(buildAlertRoleButtonId(roleName))
336
+ .setLabel(roleName)
337
+ .setStyle(ButtonStyle.Primary)
338
+ )
339
+ )
340
+ );
341
+ }
342
+
343
+ return rows;
344
+ }
345
+
346
  export function buildExportAttachment(rows) {
347
  const header = [
348
  'bet_number',
 
523
  status: status === '~' ? undefined : status,
524
  };
525
  }
526
+
527
+ export function buildAlertRoleButtonId(roleName) {
528
+ return `alert-role:${encodeURIComponent(roleName)}`;
529
+ }
530
+
531
+ export function parseAlertRoleButtonId(customId) {
532
+ const [prefix, encodedRoleName] = customId.split(':');
533
+ if (prefix !== 'alert-role' || !encodedRoleName) {
534
+ return null;
535
+ }
536
+
537
+ return decodeURIComponent(encodedRoleName);
538
+ }
src/index.js CHANGED
@@ -2,6 +2,7 @@ import http from 'node:http';
2
  import {
3
  ActionRowBuilder,
4
  ButtonStyle,
 
5
  Client,
6
  Events,
7
  GatewayIntentBits,
@@ -15,9 +16,14 @@ import { commands } from './commands.js';
15
  import { BetStore } from './db.js';
16
  import { parseBetInput } from './parser.js';
17
  import {
 
 
 
18
  BETS_PAGE_SIZE,
19
  } from './constants.js';
20
  import {
 
 
21
  buildBankrollEmbed,
22
  buildBetSavedEmbed,
23
  buildBetsEmbed,
@@ -36,6 +42,7 @@ import {
36
  buildSportsEmbed,
37
  buildSummaryEmbed,
38
  buildWelcomeEmbed,
 
39
  buildBetsPageId,
40
  parseBetsPageId,
41
  } from './embeds.js';
@@ -230,6 +237,11 @@ async function handleChatInput(interaction, store, config) {
230
  return;
231
  }
232
 
 
 
 
 
 
233
  if (commandName === 'welcome') {
234
  await handleWelcome(interaction, config);
235
  }
@@ -727,7 +739,54 @@ async function handleWelcome(interaction, config) {
727
  });
728
  }
729
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
  async function handleButton(interaction, store) {
 
 
 
 
 
 
731
  const pageState = parseBetsPageId(interaction.customId);
732
  if (!pageState) {
733
  return;
@@ -750,6 +809,53 @@ async function handleButton(interaction, store) {
750
  await handleBets(interaction, store, pageState.page, true);
751
  }
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  async function memberHasRoleName(interaction, roleName) {
754
  if (!interaction.guild) {
755
  return false;
 
2
  import {
3
  ActionRowBuilder,
4
  ButtonStyle,
5
+ ChannelType,
6
  Client,
7
  Events,
8
  GatewayIntentBits,
 
16
  import { BetStore } from './db.js';
17
  import { parseBetInput } from './parser.js';
18
  import {
19
+ ALERTS_ALLOWED_USERNAME,
20
+ ALERTS_CHANNEL_ID,
21
+ ALERT_ROLE_NAMES,
22
  BETS_PAGE_SIZE,
23
  } from './constants.js';
24
  import {
25
+ buildAlertsButtonRows,
26
+ buildAlertsEmbed,
27
  buildBankrollEmbed,
28
  buildBetSavedEmbed,
29
  buildBetsEmbed,
 
42
  buildSportsEmbed,
43
  buildSummaryEmbed,
44
  buildWelcomeEmbed,
45
+ parseAlertRoleButtonId,
46
  buildBetsPageId,
47
  parseBetsPageId,
48
  } from './embeds.js';
 
237
  return;
238
  }
239
 
240
+ if (commandName === 'alerts') {
241
+ await handleAlerts(interaction);
242
+ return;
243
+ }
244
+
245
  if (commandName === 'welcome') {
246
  await handleWelcome(interaction, config);
247
  }
 
739
  });
740
  }
741
 
742
+ async function handleAlerts(interaction) {
743
+ if (interaction.user.username !== ALERTS_ALLOWED_USERNAME) {
744
+ await interaction.reply({
745
+ embeds: [
746
+ buildErrorEmbed(
747
+ 'Not allowed',
748
+ `Only **${ALERTS_ALLOWED_USERNAME}** can use this command.`
749
+ ),
750
+ ],
751
+ flags: MessageFlags.Ephemeral,
752
+ });
753
+ return;
754
+ }
755
+
756
+ const channel = await interaction.client.channels.fetch(ALERTS_CHANNEL_ID);
757
+ if (!channel || !channel.isTextBased() || channel.type === ChannelType.DM) {
758
+ await interaction.reply({
759
+ embeds: [
760
+ buildErrorEmbed(
761
+ 'Welcome channel unavailable',
762
+ `I could not post to channel **${ALERTS_CHANNEL_ID}**.`
763
+ ),
764
+ ],
765
+ flags: MessageFlags.Ephemeral,
766
+ });
767
+ return;
768
+ }
769
+
770
+ await channel.send({
771
+ embeds: [buildAlertsEmbed()],
772
+ components: buildAlertsButtonRows(),
773
+ });
774
+
775
+ await interaction.reply({
776
+ embeds: [
777
+ buildErrorEmbed('Alerts panel posted', `Sent the analyst alert panel to <#${ALERTS_CHANNEL_ID}>.`).setColor(0x2563eb),
778
+ ],
779
+ flags: MessageFlags.Ephemeral,
780
+ });
781
+ }
782
+
783
  async function handleButton(interaction, store) {
784
+ const alertRoleName = parseAlertRoleButtonId(interaction.customId);
785
+ if (alertRoleName) {
786
+ await handleAlertRoleToggle(interaction, alertRoleName);
787
+ return;
788
+ }
789
+
790
  const pageState = parseBetsPageId(interaction.customId);
791
  if (!pageState) {
792
  return;
 
809
  await handleBets(interaction, store, pageState.page, true);
810
  }
811
 
812
+ async function handleAlertRoleToggle(interaction, roleName) {
813
+ if (!interaction.guild) {
814
+ await interaction.reply({
815
+ embeds: [buildErrorEmbed('Server only', 'Alert role buttons can only be used inside the server.')],
816
+ flags: MessageFlags.Ephemeral,
817
+ });
818
+ return;
819
+ }
820
+
821
+ if (!ALERT_ROLE_NAMES.includes(roleName)) {
822
+ await interaction.reply({
823
+ embeds: [buildErrorEmbed('Unknown alert role', 'That alert role is not configured on this bot.')],
824
+ flags: MessageFlags.Ephemeral,
825
+ });
826
+ return;
827
+ }
828
+
829
+ const member = await interaction.guild.members.fetch(interaction.user.id);
830
+ const role = interaction.guild.roles.cache.find((item) => item.name === roleName)
831
+ ?? await interaction.guild.roles.fetch().then((roles) => roles.find((item) => item.name === roleName));
832
+
833
+ if (!role) {
834
+ await interaction.reply({
835
+ embeds: [buildErrorEmbed('Role not found', `I could not find the **${roleName}** role in this server.`)],
836
+ flags: MessageFlags.Ephemeral,
837
+ });
838
+ return;
839
+ }
840
+
841
+ const hasRole = member.roles.cache.has(role.id);
842
+ if (hasRole) {
843
+ await member.roles.remove(role.id);
844
+ } else {
845
+ await member.roles.add(role.id);
846
+ }
847
+
848
+ await interaction.reply({
849
+ embeds: [
850
+ buildErrorEmbed(
851
+ hasRole ? 'Alert removed' : 'Alert added',
852
+ `${hasRole ? 'Removed' : 'Added'} **${roleName}** for <@${interaction.user.id}>.`
853
+ ).setColor(hasRole ? 0x6b7280 : 0x16a34a),
854
+ ],
855
+ flags: MessageFlags.Ephemeral,
856
+ });
857
+ }
858
+
859
  async function memberHasRoleName(interaction, roleName) {
860
  if (!interaction.guild) {
861
  return false;
test/alerts.test.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { commands } from '../src/commands.js';
4
+ import {
5
+ buildAlertsButtonRows,
6
+ buildAlertsEmbed,
7
+ buildCommandsEmbed,
8
+ buildAlertRoleButtonId,
9
+ parseAlertRoleButtonId,
10
+ } from '../src/embeds.js';
11
+
12
+ test('registers the alerts slash command', () => {
13
+ const alertsCommand = commands.find((command) => command.name === 'alerts');
14
+ assert.ok(alertsCommand);
15
+ assert.match(alertsCommand.description, /alert-role embed/i);
16
+ });
17
+
18
+ test('lists alerts in the public commands embed', () => {
19
+ const embed = buildCommandsEmbed().toJSON();
20
+ const alertsField = embed.fields.find((field) => field.name === '/alerts');
21
+
22
+ assert.ok(alertsField);
23
+ assert.match(alertsField.value, /jew_olympics/i);
24
+ });
25
+
26
+ test('builds the alerts embed with all analyst lines', () => {
27
+ const embed = buildAlertsEmbed().toJSON();
28
+
29
+ assert.match(embed.description, /We are moving from @ everyone alerts/i);
30
+ assert.match(embed.description, /@PhazzAlerts - MLB \/ NBA/);
31
+ assert.match(embed.description, /@JRAlerts - NBA/);
32
+ });
33
+
34
+ test('builds two rows of alert role buttons and round-trips custom ids', () => {
35
+ const rows = buildAlertsButtonRows();
36
+
37
+ assert.equal(rows.length, 2);
38
+ assert.equal(rows[0].components.length, 3);
39
+ assert.equal(rows[1].components.length, 3);
40
+
41
+ const customId = buildAlertRoleButtonId('RIIPAlerts');
42
+ assert.equal(parseAlertRoleButtonId(customId), 'RIIPAlerts');
43
+ });