diff --git a/src/commands/admin/points.ts b/src/commands/admin/points.ts new file mode 100644 index 0000000..f8d8de1 --- /dev/null +++ b/src/commands/admin/points.ts @@ -0,0 +1,82 @@ +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from "athena-prime"; +import database from "../../database"; + +// ---------- + +@SlashCommand( + new CommandBuilder('points', "Adjust a user's points.") + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.ManageGuild) + + .addSubcommand('give', 'Give points to a user.', builder => { + builder.addUserOption('target', 'The user to modify.', true); + builder.addNumberOption({ name: 'points', description: 'The points to give.', required: true, min_value: 1 }) + }) + .addSubcommand('take', 'Take points from a user.', builder => { + builder.addUserOption('target', 'The user to modify.', true); + builder.addNumberOption({ name: 'points', description: 'The points to take.', required: true, min_value: 1 }) + }) + .addSubcommand('set', 'Set the points of a user.', builder => { + builder.addUserOption('target', 'The user to modify.', true); + builder.addNumberOption({ name: 'points', description: 'The new points value.', required: true }) + }) +) +class RankCommand extends Command { + users = [ + '1363132678022631428', // Nix. + '454653142500507649', // Ann. + '147433323503943680' // Pond. + ]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction) { + if (!interaction.inGuild()) return; + + const target = interaction.getRequiredUser('target'); + const points = interaction.getRequiredNumber('points'); + + switch (interaction.subcommand) { + case 'give': + await database` + INSERT INTO points AS p (user_id, guild_id, points) + VALUES (${target.id}, ${interaction.guild.id}, ${points}) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = p.points + ${points} + `; + + return await interaction.createMessage({ + content: `Done! *${target.username}* has been given **${points}** points.`, + flags: Constants.MessageFlags.Ephemeral + }); + + case 'take': + await database` + INSERT INTO points AS p (user_id, guild_id, points) + VALUES ${target.id}, ${interaction.guild.id}, 0) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = GREATEST(p.points - ${points}, 0) + `; + + return await interaction.createMessage({ + content: `Done! *${target.username}* has lost **${points}** points.`, + flags: Constants.MessageFlags.Ephemeral + }); + + case 'set': + await database` + INSERT INTO points (user_id, guild_id, points) + VALUES (${target.id}, ${interaction.guild.id}, ${points}) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = ${points} + `; + + return await interaction.createMessage({ + content: `Done! *${target.username}*'s points has been set to **${points}**.`, + flags: Constants.MessageFlags.Ephemeral + }); + } + } +} + +export default new RankCommand('rank'); \ No newline at end of file diff --git a/src/commands/admin/wipe.ts b/src/commands/admin/wipe.ts new file mode 100644 index 0000000..3b8b0bb --- /dev/null +++ b/src/commands/admin/wipe.ts @@ -0,0 +1,44 @@ +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from "athena-prime"; +import database from "../../database"; + +// ---------- + +@SlashCommand( + new CommandBuilder('wipe', 'Wipes all points for the guild or specific user. THIS DOES NOT ASK FOR CONFIRMATION!') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.ManageGuild) + .addUserOption('target', "Optional. The user to wipe.", false) +) +class RankCommand extends Command { + users = [ + '1363132678022631428', // Nix. + '454653142500507649', // Ann. + '147433323503943680' // Pond. + ]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction) { + if (!interaction.inGuild()) return; + + const target = interaction.getUser('target'); + + if (!target) { + await database` UPDATE points SET points = 0 WHERE guild_id = ${interaction.guild.id}`; + + return await interaction.createMessage({ + content: 'Done! All points for this guild have been cleared.', + flags: Constants.MessageFlags.Ephemeral + }); + } + + await database` UPDATE points SET points = 0 WHERE user_id = ${target.id} AND guild_id = ${interaction.guild.id} `; + + return await interaction.createMessage({ + content: `Done! *${target.username}*'s points have been wiped.`, + flags: Constants.MessageFlags.Ephemeral + }); + } +} + +export default new RankCommand('rank'); \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index a8557da..78f1ccc 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,6 +1,8 @@ +import points from './admin/points'; +import wipe from './admin/wipe'; import leaderboard from './leaderboard'; import rank from './rank'; // ---------- -export default [leaderboard, rank]; +export default [points, wipe, leaderboard, rank]; diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index c3ad568..019b43b 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -3,6 +3,19 @@ import database from '../database'; // ---------- +async function getLeaderboard(guild_id: string, limit: number = 10): Promise<{ user_id: string; points: number }[]> { + return await database` + SELECT user_id, points + FROM points + WHERE guild_id = ${guild_id} + AND points > 0 + ORDER BY points DESC, user_id ASC + LIMIT ${limit} + `; +} + +// ---------- + @SlashCommand( new CommandBuilder('leaderboard', 'Get the petting leaderboard.') .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) @@ -14,7 +27,7 @@ class LeaderboardCommand extends Command { if (!interaction.inGuild()) return; await interaction.defer(); - const data = await database.leaderboard.getLeaderboard(interaction.guild.id); + const data = await getLeaderboard(interaction.guild.id); const description = data.length ? data .map((entry, index) => `**${index + 1}.** <@${entry.user_id}> — **${entry.points}** points!`) @@ -39,4 +52,6 @@ class LeaderboardCommand extends Command { } } +// ---------- + export default new LeaderboardCommand('leaderboard'); diff --git a/src/commands/rank.ts b/src/commands/rank.ts index 3fe5d23..d281154 100644 --- a/src/commands/rank.ts +++ b/src/commands/rank.ts @@ -3,6 +3,36 @@ import database from "../database"; // ---------- +async function getPoints(id: string, guild_id: string): Promise { + const [row] = await database` + SELECT points + FROM points + WHERE user_id = ${id} + AND guild_id = ${guild_id} + `; + + return row?.points ?? 0; +} + +async function getLeaderboardPosition(id: string, guild_id: string): Promise { + const [row] = await database` + SELECT position + FROM ( + SELECT + user_id, + RANK() OVER (ORDER BY points DESC) AS position + FROM points + WHERE guild_id = ${guild_id} + AND points > 0 + ) leaderboard + WHERE user_id = ${id} + `; + + return row?.position ?? null; +} + +// ---------- + @SlashCommand( new CommandBuilder('rank', 'See your rank information.') .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) @@ -14,8 +44,8 @@ class RankCommand extends Command { if (!interaction.inGuild()) return; await interaction.defer(); - const _points = await database.points.getPoints(interaction.member.id, interaction.guild.id); - const _rank = await database.leaderboard.getLeaderboardPosition(interaction.member.id, interaction.guild.id); + const _points = await getPoints(interaction.member.id, interaction.guild.id); + const _rank = await getLeaderboardPosition(interaction.member.id, interaction.guild.id); await interaction.createMessage({ embeds: [{ @@ -36,4 +66,6 @@ class RankCommand extends Command { } } +// ---------- + export default new RankCommand('rank'); \ No newline at end of file diff --git a/src/database.ts b/src/database.ts index b6d4def..307230e 100644 --- a/src/database.ts +++ b/src/database.ts @@ -2,91 +2,4 @@ import postgres from "postgres"; // ---------- -const client = postgres(process.env.DATABASE_URL); - -async function init() { - await client` - CREATE TABLE IF NOT EXISTS points ( - user_id TEXT NOT NULL, - guild_id TEXT NOT NULL, - points INTEGER NOT NULL DEFAULT 0 CHECK (points >= 0), - - PRIMARY KEY (user_id, guild_id) - ) - `; -} - -async function getPoints(id: string, guild_id: string): Promise { - const [row] = await client` - SELECT points - FROM points - WHERE user_id = ${id} - AND guild_id = ${guild_id} - `; - - return row?.points ?? 0; -} - -async function addPoints(id: string, guild_id: string, count: number) { - await client` - INSERT INTO points AS p (user_id, guild_id, points) - VALUES (${id}, ${guild_id}, ${count}) - ON CONFLICT (user_id, guild_id) - DO UPDATE SET points = p.points + ${count} - `; -} - -async function takePoints(id: string, guild_id: string, count: number) { - await client` - INSERT INTO points AS p (user_id, guild_id, points) - VALUES (${id}, ${guild_id}, 0) - ON CONFLICT (user_id, guild_id) - DO UPDATE SET points = GREATEST(p.points - ${count}, 0) - `; -} - -// ---------- - -async function getLeaderboard(guild_id: string, limit: number = 10): Promise<{ user_id: string; points: number }[]> { - return await client` - SELECT user_id, points - FROM points - WHERE guild_id = ${guild_id} - AND points > 0 - ORDER BY points DESC, user_id ASC - LIMIT ${limit} - `; -} - -async function getLeaderboardPosition(id: string, guild_id: string): Promise { - const [row] = await client` - SELECT position - FROM ( - SELECT - user_id, - RANK() OVER (ORDER BY points DESC) AS position - FROM points - WHERE guild_id = ${guild_id} - AND points > 0 - ) leaderboard - WHERE user_id = ${id} - `; - - return row?.position ?? null; -} - -// ---------- - -export default { - points: { - init, - getPoints, - addPoints, - takePoints - }, - - leaderboard: { - getLeaderboard, - getLeaderboardPosition - } -}; \ No newline at end of file +export default postgres(process.env.DATABASE_URL); \ No newline at end of file diff --git a/src/events/message/messageReactionAdd.ts b/src/events/message/messageReactionAdd.ts index bb0f511..c07221f 100644 --- a/src/events/message/messageReactionAdd.ts +++ b/src/events/message/messageReactionAdd.ts @@ -8,7 +8,13 @@ class MessageReactionAddEvent extends Event { async handle(context: CommandClient, message: Message, emoji: Constants.APIEmoji, member: Member) { if (!message.guildID || emoji.id != '1547870117080342548' || member.id == message.author.id) return; - await database.points.addPoints(message.author.id, member.guild.id, 1); + + await database` + INSERT INTO points AS p (user_id, guild_id, points) + VALUES (${message.author.id}, ${member.guild.id}, 1) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = p.points + 1 + `; } } diff --git a/src/events/message/messageReactionRemove.ts b/src/events/message/messageReactionRemove.ts index 4cad909..fea4204 100644 --- a/src/events/message/messageReactionRemove.ts +++ b/src/events/message/messageReactionRemove.ts @@ -8,9 +8,14 @@ class MessageReactionRemoveEvent extends Event { async handle(context: CommandClient, message: { id: string; channel: { id: string } }, emoji: Constants.APIEmoji, userId: string) { const _message = await context.getMessage(message.channel.id, message.id); - if (!_message.guildID || emoji.id != '1547870117080342548' || userId == _message.author.id) return; - await database.points.takePoints(_message.author.id, _message.guildID, 1); + + await database` + INSERT INTO points AS p (user_id, guild_id, points) + VALUES ${_message.author.id}, ${_message.guildID}, 0) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = GREATEST(p.points - 1, 0) + `; } } diff --git a/src/index.ts b/src/index.ts index f1c54bf..de0aaad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,5 +26,16 @@ events.forEach((event) => client.registerEvent(event, true)); // ---------- -database.points.init(); +database` + CREATE TABLE IF NOT EXISTS points ( + user_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + points INTEGER NOT NULL DEFAULT 0 CHECK (points >= 0), + + PRIMARY KEY (user_id, guild_id) + ) +`; + +// ---------- + client.connect();