feat: add buttons to throw results
Node.js CI / Lint and Test (push) Successful in 42s

This commit is contained in:
2025-08-15 15:00:27 -07:00
parent 2ad10115b8
commit 2544d2d15b
10 changed files with 286 additions and 9 deletions
+91
View File
@@ -0,0 +1,91 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { MessageFlags } from "discord.js";
import { checkGuildEntitlement } from "../modules/checkEntitlement.js";
import { sendUnentitledResponse } from "../modules/sendUnentitledResponse.js";
import { errorHandler } from "../utils/errorHandler.js";
import type { Button } from "../interfaces/button.js";
/**
* Handles the `/leaderboard` button.
* @param pavelle - Pavelle's instance.
* @param interaction - The interaction payload from Discord.
*/
// eslint-disable-next-line max-lines-per-function -- Lazy.
export const leaderboard: Button = async(pavelle, interaction) => {
try {
const isEntitled = await checkGuildEntitlement(pavelle, interaction.guild);
if (!isEntitled) {
await sendUnentitledResponse(interaction);
return;
}
const members = await pavelle.db.users.findMany({
where: {
serverId: interaction.guild.id,
},
});
const sorted = members.sort((a, b) => {
return b.points - a.points;
});
const topTen = sorted.slice(0, 10).map((member, index) => {
return `- **#${(index + 1).toString()}:** <@${member.userId}> - ${member.points.toString()} point(s).`;
}).
join("\n");
const yourScore = sorted.find((member) => {
return member.userId === interaction.member.id;
});
const yourRank = yourScore
? `You are rank #${(sorted.indexOf(yourScore) + 1).toString()} with ${yourScore.points.toString()} points.`
: "You are not ranked. Try throwing some stuff!";
await interaction.editReply({
allowedMentions: {
parse: [],
},
components: [
{
// eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API convention.
accent_color: null,
components: [
{
content: "# Leaderboard",
type: 10,
},
{
divider: true,
spacing: 1,
type: 14,
},
{
content: "## Top 10 Members",
type: 10,
},
{
content: topTen,
type: 10,
},
{
content: `-# ${yourRank}`,
type: 10,
},
],
spoiler: false,
type: 17,
},
],
flags: [ MessageFlags.IsComponentsV2 ],
});
} catch (error) {
const id = await errorHandler(error, "leaderboard command");
await interaction.editReply({
content:
`An error occurred while processing your request. Please try again later, or join our support server and provide this ID: ${id}`,
});
}
};
+105
View File
@@ -0,0 +1,105 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { MessageFlags } from "discord.js";
import { checkGuildEntitlement } from "../modules/checkEntitlement.js";
import { generateScore } from "../modules/generateScore.js";
import { getCachedCount } from "../modules/getCachedCount.js";
import { getConfig } from "../modules/getConfig.js";
import { getThrowComponents } from "../modules/getThrowComponents.js";
import { sendUnentitledResponse } from "../modules/sendUnentitledResponse.js";
import { errorHandler } from "../utils/errorHandler.js";
import { getRandomValue } from "../utils/getRandomValue.js";
import type { Button } from "../interfaces/button.js";
/**
* Handles the `/throw` button interaction.
* @param pavelle - Pavelle's Discord instance.
* @param interaction - The command interaction payload from Discord.
*/
// eslint-disable-next-line max-lines-per-function -- Big logic, but lotsa components
export const throwButton: Button = async(pavelle, interaction) => {
try {
const { member, guild } = interaction;
const count = getCachedCount(pavelle, `${guild.id}-${member.id}`);
if (count <= 0) {
await interaction.editReply({
components: [
{
// eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API convention.
accent_color: null,
components: [
{
content:
// eslint-disable-next-line stylistic/max-len -- long string.
"Oopsie! You are out of throws! Your count resets at the top of every hour.",
type: 10,
},
],
spoiler: false,
type: 17,
},
],
flags: [ MessageFlags.IsComponentsV2 ],
});
return;
}
const isEntitled = await checkGuildEntitlement(pavelle, guild);
if (!isEntitled) {
await sendUnentitledResponse(interaction);
return;
}
pavelle.throwCache.set(`${guild.id}-${member.id}`, count - 1);
await guild.members.fetch().catch(() => {
return null;
});
const target = getRandomValue([ ...guild.members.cache.values() ]);
const score = generateScore();
const { theme, spoiler } = await getConfig(pavelle, guild.id);
const updated = await pavelle.db.users.upsert({
create: {
points: score,
serverId: guild.id,
userId: member.id,
},
update: {
points: {
increment: score,
},
},
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention -- Prisma index convention.
serverId_userId: {
serverId: guild.id,
userId: member.id,
},
},
});
const components = getThrowComponents(
pavelle,
member.id,
target.id,
guild.id,
theme,
score,
spoiler,
updated.points,
);
await interaction.editReply({
allowedMentions: {
parse: [ ],
},
components: components,
flags: [ MessageFlags.IsComponentsV2 ],
});
} catch (error) {
const id = await errorHandler(error, "throw command");
await interaction.editReply({
content:
`An error occurred while processing your request. Please try again later, or join our support server and provide this ID: ${id}`,
});
}
};
+7
View File
@@ -5,6 +5,8 @@
*/
import { MessageFlags } from "discord.js";
import { checkGuildEntitlement } from "../modules/checkEntitlement.js";
import { sendUnentitledResponse } from "../modules/sendUnentitledResponse.js";
import { errorHandler } from "../utils/errorHandler.js";
import type { Command } from "../interfaces/command.js";
@@ -16,6 +18,11 @@ import type { Command } from "../interfaces/command.js";
// eslint-disable-next-line max-lines-per-function -- Lazy.
export const leaderboard: Command = async(pavelle, interaction) => {
try {
const isEntitled = await checkGuildEntitlement(pavelle, interaction.guild);
if (!isEntitled) {
await sendUnentitledResponse(interaction);
return;
}
const members = await pavelle.db.users.findMany({
where: {
serverId: interaction.guild.id,
+20 -3
View File
@@ -4,22 +4,39 @@
* @author Naomi Carrigan
*/
import { leaderboard as leaderboardButton } from "../buttons/leaderboard.js";
import { throwButton } from "../buttons/throwButton.js";
import { about } from "../commands/about.js";
import { config } from "../commands/config.js";
import { leaderboard } from "../commands/leaderboard.js";
import { throwCmd } from "../commands/throwCmd.js";
import type { Button } from "../interfaces/button.js";
import type { Command } from "../interfaces/command.js";
const defaultHandler: Command = async(_lynira, interaction) => {
const defaultCommandHandler: Command = async(_lynira, interaction) => {
await interaction.editReply({
content: "This command is not implemented yet.",
});
};
export const handlers: { _default: Command } & Record<string, Command> = {
_default: defaultHandler,
const defaultButtonHandler: Button = async(_pavelle, interaction) => {
await interaction.editReply({
content: "This button is not implemented yet.",
});
};
const commandHandlers: { _default: Command } & Record<string, Command> = {
_default: defaultCommandHandler,
about: about,
config: config,
leaderboard: leaderboard,
throw: throwCmd,
};
const buttonHandlers: { _default: Button } & Record<string, Button> = {
_default: defaultButtonHandler,
leaderboard: leaderboardButton,
throw: throwButton,
};
export { commandHandlers, buttonHandlers };
+7 -4
View File
@@ -11,6 +11,7 @@ import {
Events,
} from "discord.js";
import { scheduleJob } from "node-schedule";
import { processButton } from "./modules/processButton.js";
import { processCommand } from "./modules/processCommand.js";
import { instantiateServer } from "./server/serve.js";
import { logger } from "./utils/logger.js";
@@ -32,13 +33,15 @@ pavelle.discord.once(Events.ClientReady, () => {
});
pavelle.discord.on(Events.InteractionCreate, (interaction) => {
if (!interaction.isChatInputCommand()) {
return;
}
if (!interaction.inCachedGuild()) {
return;
}
void processCommand(pavelle, interaction);
if (interaction.isChatInputCommand()) {
void processCommand(pavelle, interaction);
}
if (interaction.isButton()) {
void processButton(pavelle, interaction);
}
});
pavelle.discord.on(Events.Error, (error) => {
+12
View File
@@ -0,0 +1,12 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Pavelle } from "./pavelle.js";
import type { ButtonInteraction } from "discord.js";
export type Button = (
pavelle: Pavelle,
interaction: ButtonInteraction<"cached">
)=> Promise<void>;
+21
View File
@@ -144,6 +144,27 @@ export const getThrowComponents = (
content: `-# You now have ${total.toString()} point(s).\n-# You have ${count.toString()} remaining throws for the hour.`,
type: 10,
},
{
components: [
{
// eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API convention.
custom_id: "throw",
disabled: false,
label: "Throw another!",
style: 3,
type: 2,
},
{
// eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API convention.
custom_id: "leaderboard",
disabled: false,
label: "Check leaderboard",
style: 1,
type: 2,
},
],
type: 1,
},
],
spoiler: false,
type: 17,
+20
View File
@@ -0,0 +1,20 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { buttonHandlers as handlers } from "../config/handlers.js";
import type { Button } from "../interfaces/button.js";
/**
* Process a button interaction.
* @param pavelle - The Pavelle instance.
* @param interaction - The interaction to process.
*/
export const processButton: Button = async(pavelle, interaction) => {
await interaction.deferReply();
const commandName = interaction.customId;
// eslint-disable-next-line no-underscore-dangle -- Accessing private property for command handler.
await (handlers[commandName] ?? handlers._default)(pavelle, interaction);
};
+1 -1
View File
@@ -4,7 +4,7 @@
* @author Naomi Carrigan
*/
import { handlers } from "../config/handlers.js";
import { commandHandlers as handlers } from "../config/handlers.js";
import type { Command } from "../interfaces/command.js";
/**
+2 -1
View File
@@ -11,6 +11,7 @@ import {
MessageFlags,
TextDisplayBuilder,
type ChatInputCommandInteraction,
type ButtonInteraction,
} from "discord.js";
/**
@@ -18,7 +19,7 @@ import {
* @param interaction - The interaction object from Discord.
*/
export const sendUnentitledResponse = async(
interaction: ChatInputCommandInteraction,
interaction: ChatInputCommandInteraction | ButtonInteraction,
): Promise<void> => {
const components = [
new TextDisplayBuilder().setContent(