Codex commited on
Commit ·
8960b50
1
Parent(s): 7695308
Add alerts role panel command
Browse files- src/commands.js +3 -0
- src/constants.js +11 -0
- src/embeds.js +51 -0
- src/index.js +106 -0
- 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 |
+
});
|