From 8b00a8a876c1ec347b1da2b74ea29f6b50f9edd7 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Sun, 13 Sep 2026 15:58:06 +0800 Subject: [PATCH] (rewrite): Pet the Pond now does petting for all. --- src/commands/leaderboard.ts | 3 +- src/commands/rank.ts | 5 +- src/commands/settings.ts | 48 ------ src/database.ts | 163 ++++++-------------- src/events/guild/guildCreate.ts | 14 -- src/events/guild/guildDelete.ts | 15 -- src/events/index.ts | 4 - src/events/message/messageReactionAdd.ts | 7 +- src/events/message/messageReactionRemove.ts | 11 +- src/index.ts | 4 +- tsconfig.json | 2 +- src/types.d.ts => types/env.d.ts | 8 +- 12 files changed, 63 insertions(+), 221 deletions(-) delete mode 100644 src/commands/settings.ts delete mode 100644 src/events/guild/guildCreate.ts delete mode 100644 src/events/guild/guildDelete.ts rename src/types.d.ts => types/env.d.ts (57%) diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index 934ef80..c3ad568 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -12,13 +12,12 @@ import database from '../database'; class LeaderboardCommand extends Command { async handleCommand(context: CommandClient, interaction: CommandInteraction) { if (!interaction.inGuild()) return; - await interaction.defer(); const data = await database.leaderboard.getLeaderboard(interaction.guild.id); const description = data.length ? data - .map((entry, index) => `**${index + 1}.** <@${entry.userId}> — **${entry.points}** points!`) + .map((entry, index) => `**${index + 1}.** <@${entry.user_id}> — **${entry.points}** points!`) .join('\n') : 'Nobody has any points yet!'; diff --git a/src/commands/rank.ts b/src/commands/rank.ts index 8b16b5f..3fe5d23 100644 --- a/src/commands/rank.ts +++ b/src/commands/rank.ts @@ -12,11 +12,10 @@ import database from "../database"; class RankCommand extends Command { async handleCommand(context: CommandClient, interaction: CommandInteraction) { if (!interaction.inGuild()) return; - await interaction.defer(); - const _points = await database.points.getOrCreatePoints(interaction.guild.id, interaction.member.id); - const _rank = await database.leaderboard.getLeaderboardPosition(interaction.guild.id, interaction.member.id); + const _points = await database.points.getPoints(interaction.member.id, interaction.guild.id); + const _rank = await database.leaderboard.getLeaderboardPosition(interaction.member.id, interaction.guild.id); await interaction.createMessage({ embeds: [{ diff --git a/src/commands/settings.ts b/src/commands/settings.ts deleted file mode 100644 index f8575d5..0000000 --- a/src/commands/settings.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from "athena-prime"; -import database from "../database"; - -// ---------- - -@SlashCommand( - new CommandBuilder('settings', 'Configure guild-level settings.') - .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) - .setContexts(Constants.InteractionContextType.Guild) - .setCommandType(Constants.ApplicationCommandType.ChatInput) - .setMemberPermission(Constants.PermissionFlagsBits.ManageGuild) - .addSubcommand('emoji', 'Set the emoji used for petting.', (builder) => { - builder.addStringOption({ - name: 'value', - description: 'The custom emoji ID to use.', - required: true, - min_length: 17, - max_length: 20, - }); - }) -) -class SettingsCommand extends Command { - userPermissions = [Constants.PermissionFlagsBits.ManageGuild]; - - async handleCommand(context: CommandClient, interaction: CommandInteraction) { - if (!interaction.inGuild() || !interaction.subcommand) return; - await interaction.defer(true); - - switch (interaction.subcommand) { - case 'emoji': - const config = await database.config.getOrCreateConfig(interaction.guild.id); - const emojiId = interaction.getRequiredString('value'); - - if (!/^\d{17,20}$/.test(emojiId)) { - return await interaction.createMessage({ content: 'Please provide a valid custom emoji ID.' }); - } - - await database.config.updateConfig(interaction.guild.id, { - ...config, - emoji_id: emojiId, - }); - - return await interaction.createMessage({ content: `The reaction emoji ID has been set to ${emojiId}.` }); - } - } -} - -export default new SettingsCommand('settings'); \ No newline at end of file diff --git a/src/database.ts b/src/database.ts index 3aade6d..59eb656 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,153 +1,90 @@ -import postgres, { Sql } from 'postgres'; +import postgres from "postgres"; // ---------- -const client: Sql = postgres(process.env.DATABASE_URL); +const client = postgres(process.env.DATABASE_URL); -export async function init(): Promise { +async function init() { await client` - CREATE TABLE IF NOT EXISTS guilds ( - guild_id TEXT PRIMARY KEY, - config JSONB NOT NULL default '{}'::jsonb - );` - - await client` - CREATE TABLE IF NOT EXISTS points ( - guild_id TEXT NOT NULL, + CREATE TABLE IF NOT EXISTS user_points ( user_id TEXT NOT NULL, - points BIGINT NOT NULL DEFAULT 0, + guild_id TEXT NOT NULL, + points INTEGER NOT NULL DEFAULT 0 CHECK (points >= 0), - PRIMARY KEY (guild_id, user_id), - - CONSTRAINT fk_points_guild - FOREIGN KEY (guild_id) - REFERENCES guilds (guild_id) - ON DELETE CASCADE - ); + PRIMARY KEY (user_id, guild_id) + ) `; } -//#region Configuration - -export async function getOrCreateConfig(guildId: string): Promise { - const [inserted] = await client<{ config: Application.Configuration }[]>` - INSERT INTO guilds (guild_id) - VALUES (${guildId}) - ON CONFLICT (guild_id) DO NOTHING - RETURNING config +async function getPoints(id: string, guild_id: string): Promise { + const [row] = await client` + SELECT points + FROM user_points + WHERE user_id = ${id} + AND guild_id = ${guild_id} `; - if (inserted) return inserted.config; - - const [row] = await client<{ config: Application.Configuration }[]>` - SELECT config FROM guilds - WHERE guild_id = ${guildId} - `; - - return row.config; + return row?.points ?? 0; } -export async function updateConfig(guildId: string, config: Application.Configuration): Promise { +async function addPoints(id: string, guild_id: string, count: number) { await client` - UPDATE guilds SET config = ${client.json(config)} - WHERE guild_id = ${guildId} + INSERT INTO user_points (user_id, guild_id, points) + VALUES (${id}, ${guild_id}, ${count}) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = user_points.points + ${count} `; } -export async function deleteConfig(guildId: string): Promise { +async function takePoints(id: string, guild_id: string, count: number) { await client` - DELETE FROM guilds - WHERE guild_id = ${guildId} + INSERT INTO user_points (user_id, guild_id, points) + VALUES (${id}, ${guild_id}, 0) + ON CONFLICT (user_id, guild_id) + DO UPDATE SET points = GREATEST(user_points.points - ${count}, 0) `; } -//#endregion +// ---------- -//#region Points - -export async function getOrCreatePoints(guildId: string, userId: string): Promise { - const [inserted] = await client<{ points: bigint }[]>` - INSERT INTO points (guild_id, user_id) - VALUES (${guildId}, ${userId}) - ON CONFLICT (guild_id, user_id) DO NOTHING - RETURNING points - `; - - if (inserted) return inserted.points; - - const [row] = await client<{ points: bigint }[]>` - SELECT points FROM points - WHERE guild_id = ${guildId} AND user_id = ${userId} - `; - - return row.points; -} - -export async function updatePoints(guildId: string, userId: string, points: bigint): Promise { - await client` - UPDATE points SET points = ${points} - WHERE guild_id = ${guildId} AND user_id = ${userId} - `; -} - -export async function deletePoints(guildId: string, userId: string): Promise { - await client` - DELETE FROM points - WHERE guild_id = ${guildId} AND user_id = ${userId} - `; -} - -//#endregion - -//#region Leaderboard - -export async function getLeaderboard(guildId: string, limit: number = 10): Promise<{ userId: string; points: bigint }[]> { - return client<{ userId: string; points: bigint }[]>` - SELECT user_id AS "userId", points FROM points - WHERE guild_id = ${guildId} AND points > 0 +async function getLeaderboard(guild_id: string, limit: number = 10): Promise<{ user_id: string; points: number }[]> { + return await client` + SELECT user_id, points + FROM user_points + WHERE guild_id = ${guild_id} ORDER BY points DESC, user_id ASC LIMIT ${limit} `; } -export async function getLeaderboardPosition(guildId: string, userId: string): Promise { - const [row] = await client<{ rank: number | null }[]>` - WITH target AS ( - SELECT points FROM points - WHERE guild_id = ${guildId} AND user_id = ${userId} - ) - SELECT CASE WHEN COUNT(target.*) = 0 THEN NULL ELSE COUNT(candidate.*)::int + 1 END AS rank - FROM target - LEFT JOIN points AS candidate - ON candidate.guild_id = ${guildId} - AND ( - candidate.points > target.points - OR (candidate.points = target.points AND candidate.user_id < ${userId}) - ) +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 user_points + WHERE guild_id = ${guild_id} + ) leaderboard + WHERE user_id = ${id} `; - return row.rank; + return row?.position ?? null; } -//#endregion +// ---------- export default { - init, - - config: { - getOrCreateConfig, - updateConfig, - deleteConfig - }, - points: { - getOrCreatePoints, - updatePoints, - deletePoints + init, + getPoints, + addPoints, + takePoints }, leaderboard: { getLeaderboard, - getLeaderboardPosition, + getLeaderboardPosition } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/events/guild/guildCreate.ts b/src/events/guild/guildCreate.ts deleted file mode 100644 index 4712072..0000000 --- a/src/events/guild/guildCreate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CommandClient, Event, Guild } from 'athena-prime'; -import database from '../../database'; - -// ---------- - -class GuildCreateEvent extends Event { - event: string = 'guildCreate' as const; - - async handle(context: CommandClient, guild: Guild) { - await database.config.getOrCreateConfig(guild.id); - } -} - -export default new GuildCreateEvent('guildCreate'); diff --git a/src/events/guild/guildDelete.ts b/src/events/guild/guildDelete.ts deleted file mode 100644 index 577f284..0000000 --- a/src/events/guild/guildDelete.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CommandClient, Event, Guild } from 'athena-prime'; -import database from '../../database'; - -// ---------- - -class GuildCreateEvent extends Event { - event: string = 'guildDelete' as const; - - // TODO: Implement weekly config cleanup task instead. - async handle(context: CommandClient, guild: Guild | { id: string }) { - await database.config.deleteConfig(guild.id); - } -} - -export default new GuildCreateEvent('guildDelete'); diff --git a/src/events/index.ts b/src/events/index.ts index c7175cb..8b6513d 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,5 +1,3 @@ -import guildCreate from './guild/guildCreate'; -import guildDelete from './guild/guildDelete'; import messageReactionAdd from './message/messageReactionAdd'; import messageReactionRemove from './message/messageReactionRemove'; import ready from './client/ready'; @@ -7,8 +5,6 @@ import ready from './client/ready'; // ---------- export default [ - guildCreate, - guildDelete, messageReactionAdd, messageReactionRemove, ready, diff --git a/src/events/message/messageReactionAdd.ts b/src/events/message/messageReactionAdd.ts index 61aa74d..a98007b 100644 --- a/src/events/message/messageReactionAdd.ts +++ b/src/events/message/messageReactionAdd.ts @@ -7,11 +7,8 @@ class MessageReactionAddEvent extends Event { event: string = 'messageReactionAdd' as const; async handle(context: CommandClient, message: Message, emoji: Constants.APIEmoji, member: Member) { - const _config = await database.config.getOrCreateConfig(member.guild.id); - if (emoji.id !== _config.emoji_id) return; - - const _entry = await database.points.getOrCreatePoints(member.guild.id, member.id); - await database.points.updatePoints(member.guild.id, member.id, _entry + 1n); + if (emoji.id !== "448912932340891670") return; + await database.points.addPoints(message.author.id, member.guild.id, 1); } } diff --git a/src/events/message/messageReactionRemove.ts b/src/events/message/messageReactionRemove.ts index 72f46eb..cf3e93a 100644 --- a/src/events/message/messageReactionRemove.ts +++ b/src/events/message/messageReactionRemove.ts @@ -7,14 +7,11 @@ class MessageReactionRemoveEvent extends Event { event: string = 'messageReactionRemove' as const; async handle(context: CommandClient, message: { id: string; channel: { id: string } }, emoji: Constants.APIEmoji, userId: string) { - const _msg = await context.getMessage(message.channel.id, message.id); - if (!_msg.guildID) return; + const _message = await context.getMessage(message.channel.id, message.id); + if (!_message.guildID) return; - const _config = await database.config.getOrCreateConfig(_msg.guildID); - if (emoji.id !== _config.emoji_id) return; - - const _entry = await database.points.getOrCreatePoints(_msg.guildID, userId); - await database.points.updatePoints(_msg.guildID, userId, _entry - 1n); + if (emoji.id !== "448912932340891670") return; + await database.points.takePoints(_message.author.id, _message.guildID, 1); } } diff --git a/src/index.ts b/src/index.ts index 8400816..f1c54bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; -import { CommandClient, Constants, Guild, Member, Message, NullCollection, User } from 'athena-prime'; +import { CommandClient, Constants } from 'athena-prime'; import commands from './commands'; import database from './database'; import events from './events'; @@ -26,5 +26,5 @@ events.forEach((event) => client.registerEvent(event, true)); // ---------- -database.init(); +database.points.init(); client.connect(); diff --git a/tsconfig.json b/tsconfig.json index d125b7b..10d41fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "experimentalDecorators": true, }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "types/**/*.ts"], } diff --git a/src/types.d.ts b/types/env.d.ts similarity index 57% rename from src/types.d.ts rename to types/env.d.ts index 532d5cb..402a839 100644 --- a/src/types.d.ts +++ b/types/env.d.ts @@ -1,10 +1,4 @@ declare global { - namespace Application { - type Configuration = { - emoji_id: string; - }; - } - namespace NodeJS { interface ProcessEnv { DATABASE_URL: string; @@ -13,4 +7,4 @@ declare global { } } -export { }; +export {}; \ No newline at end of file