mirror of
https://github.com/nx-bat/pet-the-pond.git
synced 2026-09-22 10:29:11 -04:00
(rewrite): Pet the Pond now does petting for all.
This commit is contained in:
@@ -12,13 +12,12 @@ import database from '../database';
|
||||
class LeaderboardCommand extends Command<CommandClient> {
|
||||
async handleCommand(context: CommandClient<any, any>, 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!';
|
||||
|
||||
|
||||
@@ -12,11 +12,10 @@ import database from "../database";
|
||||
class RankCommand extends Command<CommandClient> {
|
||||
async handleCommand(context: CommandClient<any, any>, 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: [{
|
||||
|
||||
@@ -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<CommandClient> {
|
||||
userPermissions = [Constants.PermissionFlagsBits.ManageGuild];
|
||||
|
||||
async handleCommand(context: CommandClient<any, any>, 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');
|
||||
+50
-113
@@ -1,153 +1,90 @@
|
||||
import postgres, { Sql } from 'postgres';
|
||||
import postgres from "postgres";
|
||||
|
||||
// ----------
|
||||
|
||||
const client: Sql<any> = postgres(process.env.DATABASE_URL);
|
||||
const client = postgres(process.env.DATABASE_URL);
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
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<Application.Configuration> {
|
||||
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<number> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<bigint> {
|
||||
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<void> {
|
||||
await client`
|
||||
UPDATE points SET points = ${points}
|
||||
WHERE guild_id = ${guildId} AND user_id = ${userId}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function deletePoints(guildId: string, userId: string): Promise<void> {
|
||||
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<number | null> {
|
||||
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<number | null> {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { CommandClient, Event, Guild } from 'athena-prime';
|
||||
import database from '../../database';
|
||||
|
||||
// ----------
|
||||
|
||||
class GuildCreateEvent extends Event<CommandClient> {
|
||||
event: string = 'guildCreate' as const;
|
||||
|
||||
async handle(context: CommandClient<any, any>, guild: Guild) {
|
||||
await database.config.getOrCreateConfig(guild.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GuildCreateEvent('guildCreate');
|
||||
@@ -1,15 +0,0 @@
|
||||
import { CommandClient, Event, Guild } from 'athena-prime';
|
||||
import database from '../../database';
|
||||
|
||||
// ----------
|
||||
|
||||
class GuildCreateEvent extends Event<CommandClient> {
|
||||
event: string = 'guildDelete' as const;
|
||||
|
||||
// TODO: Implement weekly config cleanup task instead.
|
||||
async handle(context: CommandClient<any, any>, guild: Guild | { id: string }) {
|
||||
await database.config.deleteConfig(guild.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GuildCreateEvent('guildDelete');
|
||||
@@ -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,
|
||||
|
||||
@@ -7,11 +7,8 @@ class MessageReactionAddEvent extends Event<CommandClient> {
|
||||
event: string = 'messageReactionAdd' as const;
|
||||
|
||||
async handle(context: CommandClient<any, any>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ class MessageReactionRemoveEvent extends Event<CommandClient> {
|
||||
event: string = 'messageReactionRemove' as const;
|
||||
|
||||
async handle(context: CommandClient<any, any>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
declare global {
|
||||
namespace Application {
|
||||
type Configuration = {
|
||||
emoji_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
DATABASE_URL: string;
|
||||
DISCORD_TOKEN: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
Reference in New Issue
Block a user