mirror of
https://github.com/nx-bat/pet-the-pond.git
synced 2026-09-22 10:29:11 -04:00
Compare commits
9 Commits
016585ddf9
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| ef5a59205a | |||
| b32278c108 | |||
| b7b26796bd | |||
| 145e0f7365 | |||
| b6d4c9b4d2 | |||
| 141f68ba5e | |||
| b3521938f5 | |||
| e5339b9b14 | |||
| 8b00a8a876 |
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
# Default database credentials are used. This should be adjusted based on your own postgres configuration.
|
# Default database credentials are used. This should be adjusted based on your own postgres configuration.
|
||||||
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
|
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/pond
|
||||||
DEV_GUILD=
|
DEV_GUILD=
|
||||||
DISCORD_TOKEN=
|
DISCORD_TOKEN=
|
||||||
|
LOGGING_CHANNEL=
|
||||||
@@ -4,3 +4,4 @@ node_modules/
|
|||||||
|
|
||||||
# Files.
|
# Files.
|
||||||
.env
|
.env
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -1,2 +1,12 @@
|
|||||||
# Pet the Pond
|
# Pond's Petting Zoo
|
||||||
A points-based reaction bot for Discord. If anyone wants to take this project off my hands, please take it. I don't want it anymore.
|
A small joke bot which counts petting reactions on messages, giving points to the message author & placing them on the leaderboard.
|
||||||
|
It includes some basic commands which are restricted to specific users by default.
|
||||||
|
|
||||||
|
Please note this is was a "for the meme" request and isn't supposed to be used by other servers. Use it however you want though.
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
These are packages I mainly use due to them not being a total headache to use and don't require a million packages.
|
||||||
|
|
||||||
|
- `athena-prime` - A modified fork of [Eris](https://github.com/abalabahaha/eris), maintained by [Team Hydra](https://teamhydra.dev/).
|
||||||
|
- [`postgres`](https://github.com/porsager/postgres) - A postgres client written in JavaScript. The only one I will use.
|
||||||
|
- [`dotenv`](https://github.com/motdotla/dotenv) - A library for loading variables from .env files.
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: pond
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -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<CommandClient> {
|
||||||
|
users = [
|
||||||
|
'1363132678022631428', // Nix.
|
||||||
|
'454653142500507649', // Ann.
|
||||||
|
'147433323503943680' // Pond.
|
||||||
|
];
|
||||||
|
|
||||||
|
async handleCommand(context: CommandClient<any, any>, 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');
|
||||||
@@ -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<CommandClient> {
|
||||||
|
users = [
|
||||||
|
'1363132678022631428', // Nix.
|
||||||
|
'454653142500507649', // Ann.
|
||||||
|
'147433323503943680' // Pond.
|
||||||
|
];
|
||||||
|
|
||||||
|
async handleCommand(context: CommandClient<any, any>, 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');
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import points from './admin/points';
|
||||||
|
import wipe from './admin/wipe';
|
||||||
import leaderboard from './leaderboard';
|
import leaderboard from './leaderboard';
|
||||||
import rank from './rank';
|
import rank from './rank';
|
||||||
import settings from './settings';
|
|
||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
export default [leaderboard, rank, settings];
|
export default [points, wipe, leaderboard, rank];
|
||||||
|
|||||||
@@ -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(
|
@SlashCommand(
|
||||||
new CommandBuilder('leaderboard', 'Get the petting leaderboard.')
|
new CommandBuilder('leaderboard', 'Get the petting leaderboard.')
|
||||||
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
|
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
|
||||||
@@ -12,13 +25,12 @@ import database from '../database';
|
|||||||
class LeaderboardCommand extends Command<CommandClient> {
|
class LeaderboardCommand extends Command<CommandClient> {
|
||||||
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction) {
|
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction) {
|
||||||
if (!interaction.inGuild()) return;
|
if (!interaction.inGuild()) return;
|
||||||
|
|
||||||
await interaction.defer();
|
await interaction.defer();
|
||||||
|
|
||||||
const data = await database.leaderboard.getLeaderboard(interaction.guild.id);
|
const data = await getLeaderboard(interaction.guild.id);
|
||||||
const description = data.length
|
const description = data.length
|
||||||
? data
|
? data
|
||||||
.map((entry, index) => `**${index + 1}.** <@${entry.userId}> — **${entry.points}** points!`)
|
.map((entry, index) => `**${index + 1}.** <@${entry.user_id}> — **${entry.points}** points!`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
: 'Nobody has any points yet!';
|
: 'Nobody has any points yet!';
|
||||||
|
|
||||||
@@ -40,4 +52,6 @@ class LeaderboardCommand extends Command<CommandClient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
|
||||||
export default new LeaderboardCommand('leaderboard');
|
export default new LeaderboardCommand('leaderboard');
|
||||||
|
|||||||
+34
-3
@@ -3,6 +3,36 @@ import database from "../database";
|
|||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
|
async function getPoints(id: string, guild_id: string): Promise<number> {
|
||||||
|
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<number | null> {
|
||||||
|
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(
|
@SlashCommand(
|
||||||
new CommandBuilder('rank', 'See your rank information.')
|
new CommandBuilder('rank', 'See your rank information.')
|
||||||
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
|
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
|
||||||
@@ -12,11 +42,10 @@ import database from "../database";
|
|||||||
class RankCommand extends Command<CommandClient> {
|
class RankCommand extends Command<CommandClient> {
|
||||||
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction) {
|
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction) {
|
||||||
if (!interaction.inGuild()) return;
|
if (!interaction.inGuild()) return;
|
||||||
|
|
||||||
await interaction.defer();
|
await interaction.defer();
|
||||||
|
|
||||||
const _points = await database.points.getOrCreatePoints(interaction.guild.id, interaction.member.id);
|
const _points = await getPoints(interaction.member.id, interaction.guild.id);
|
||||||
const _rank = await database.leaderboard.getLeaderboardPosition(interaction.guild.id, interaction.member.id);
|
const _rank = await getLeaderboardPosition(interaction.member.id, interaction.guild.id);
|
||||||
|
|
||||||
await interaction.createMessage({
|
await interaction.createMessage({
|
||||||
embeds: [{
|
embeds: [{
|
||||||
@@ -37,4 +66,6 @@ class RankCommand extends Command<CommandClient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
|
||||||
export default new RankCommand('rank');
|
export default new RankCommand('rank');
|
||||||
@@ -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');
|
|
||||||
+2
-150
@@ -1,153 +1,5 @@
|
|||||||
import postgres, { Sql } from 'postgres';
|
import postgres from "postgres";
|
||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
const client: Sql<any> = postgres(process.env.DATABASE_URL);
|
export default postgres(process.env.DATABASE_URL);
|
||||||
|
|
||||||
export async function init(): Promise<void> {
|
|
||||||
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,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
points BIGINT NOT NULL DEFAULT 0,
|
|
||||||
|
|
||||||
PRIMARY KEY (guild_id, user_id),
|
|
||||||
|
|
||||||
CONSTRAINT fk_points_guild
|
|
||||||
FOREIGN KEY (guild_id)
|
|
||||||
REFERENCES guilds (guild_id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#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
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (inserted) return inserted.config;
|
|
||||||
|
|
||||||
const [row] = await client<{ config: Application.Configuration }[]>`
|
|
||||||
SELECT config FROM guilds
|
|
||||||
WHERE guild_id = ${guildId}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return row.config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateConfig(guildId: string, config: Application.Configuration): Promise<void> {
|
|
||||||
await client`
|
|
||||||
UPDATE guilds SET config = ${client.json(config)}
|
|
||||||
WHERE guild_id = ${guildId}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteConfig(guildId: string): Promise<void> {
|
|
||||||
await client`
|
|
||||||
DELETE FROM guilds
|
|
||||||
WHERE guild_id = ${guildId}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#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
|
|
||||||
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})
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
return row.rank;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
export default {
|
|
||||||
init,
|
|
||||||
|
|
||||||
config: {
|
|
||||||
getOrCreateConfig,
|
|
||||||
updateConfig,
|
|
||||||
deleteConfig
|
|
||||||
},
|
|
||||||
|
|
||||||
points: {
|
|
||||||
getOrCreatePoints,
|
|
||||||
updatePoints,
|
|
||||||
deletePoints
|
|
||||||
},
|
|
||||||
|
|
||||||
leaderboard: {
|
|
||||||
getLeaderboard,
|
|
||||||
getLeaderboardPosition,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { CommandClient, Event } from 'athena-prime';
|
||||||
|
|
||||||
|
// ----------
|
||||||
|
|
||||||
|
class ErrorEvent extends Event<CommandClient> {
|
||||||
|
event: string = 'error' as const;
|
||||||
|
|
||||||
|
async handle(context: CommandClient<any, any>, error: string | Error, shard?: number | undefined) {
|
||||||
|
const _err = error as Error;
|
||||||
|
|
||||||
|
await context.createMessage(process.env.LOGGING_CHANNEL, {
|
||||||
|
embed: {
|
||||||
|
color: 0xff5555,
|
||||||
|
|
||||||
|
title: 'Error',
|
||||||
|
|
||||||
|
fields: [
|
||||||
|
{ name: 'Message', value: _err.message, inline: false },
|
||||||
|
{ name: 'Cause', value: `${_err.cause ?? 'N/A'}`, inline: false },
|
||||||
|
{ name: 'Stack', value: `${_err.stack ?? 'N/A'}`, inline: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ErrorEvent('error');
|
||||||
@@ -7,7 +7,20 @@ class ReadyEvent extends Event<CommandClient> {
|
|||||||
|
|
||||||
async handle(context: CommandClient<any, any>) {
|
async handle(context: CommandClient<any, any>) {
|
||||||
await context.deployCommands();
|
await context.deployCommands();
|
||||||
context.setCustomActivity('Petting the Pond!');
|
context.setCustomActivity('Tracking your petting!');
|
||||||
|
|
||||||
|
await context.createMessage(process.env.LOGGING_CHANNEL, {
|
||||||
|
embed: {
|
||||||
|
color: 0x55ff55,
|
||||||
|
|
||||||
|
title: 'Bot Ready',
|
||||||
|
|
||||||
|
fields: [
|
||||||
|
{ name: 'Guilds', value: `${context.guilds.size}`, inline: true },
|
||||||
|
{ name: 'Shards', value: `${context.shards.size}`, inline: true },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CommandClient, Event, Guild } from 'athena-prime';
|
import { CommandClient, Event, Guild } from 'athena-prime';
|
||||||
import database from '../../database';
|
|
||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
@@ -7,7 +6,25 @@ class GuildCreateEvent extends Event<CommandClient> {
|
|||||||
event: string = 'guildCreate' as const;
|
event: string = 'guildCreate' as const;
|
||||||
|
|
||||||
async handle(context: CommandClient<any, any>, guild: Guild) {
|
async handle(context: CommandClient<any, any>, guild: Guild) {
|
||||||
await database.config.getOrCreateConfig(guild.id);
|
await context.createMessage(process.env.LOGGING_CHANNEL, {
|
||||||
|
embed: {
|
||||||
|
color: 0x55ff55,
|
||||||
|
|
||||||
|
thumbnail: {
|
||||||
|
url: guild.iconURL!,
|
||||||
|
},
|
||||||
|
|
||||||
|
title: 'Joined Guild',
|
||||||
|
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', value: `${guild.name}`, inline: false },
|
||||||
|
{ name: 'Name', value: `${guild.id}`, inline: false },
|
||||||
|
{ name: 'Owner', value: `${(await context.fetchUser(guild.ownerID)).username}`, inline: false },
|
||||||
|
{ name: 'Members', value: `${guild.memberCount}`, inline: false },
|
||||||
|
{ name: 'Large', value: `${guild.large}`, inline: false },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { CommandClient, Event, Guild } from 'athena-prime';
|
import { CommandClient, Event, Guild } from 'athena-prime';
|
||||||
import database from '../../database';
|
|
||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
class GuildCreateEvent extends Event<CommandClient> {
|
class GuildCreateEvent extends Event<CommandClient> {
|
||||||
event: string = 'guildDelete' as const;
|
event: string = 'guildCreate' as const;
|
||||||
|
|
||||||
// TODO: Implement weekly config cleanup task instead.
|
async handle(context: CommandClient<any, any>, guild: { id: string; }) {
|
||||||
async handle(context: CommandClient<any, any>, guild: Guild | { id: string }) {
|
await context.createMessage(process.env.LOGGING_CHANNEL, {
|
||||||
await database.config.deleteConfig(guild.id);
|
embed: {
|
||||||
|
color: 0xff5555,
|
||||||
|
|
||||||
|
title: 'Left Guild',
|
||||||
|
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', value: `${guild.id}`, inline: false }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new GuildCreateEvent('guildDelete');
|
export default new GuildCreateEvent('guildCreate');
|
||||||
|
|||||||
+7
-6
@@ -1,15 +1,16 @@
|
|||||||
|
import error from './client/error';
|
||||||
|
import ready from './client/ready';
|
||||||
|
|
||||||
import guildCreate from './guild/guildCreate';
|
import guildCreate from './guild/guildCreate';
|
||||||
import guildDelete from './guild/guildDelete';
|
import guildDelete from './guild/guildDelete';
|
||||||
|
|
||||||
import messageReactionAdd from './message/messageReactionAdd';
|
import messageReactionAdd from './message/messageReactionAdd';
|
||||||
import messageReactionRemove from './message/messageReactionRemove';
|
import messageReactionRemove from './message/messageReactionRemove';
|
||||||
import ready from './client/ready';
|
|
||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
guildCreate,
|
error, ready, // Client.
|
||||||
guildDelete,
|
guildCreate, guildDelete, // Guild.
|
||||||
messageReactionAdd,
|
messageReactionAdd, messageReactionRemove // Message.
|
||||||
messageReactionRemove,
|
|
||||||
ready,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ class MessageReactionAddEvent extends Event<CommandClient> {
|
|||||||
event: string = 'messageReactionAdd' as const;
|
event: string = 'messageReactionAdd' as const;
|
||||||
|
|
||||||
async handle(context: CommandClient<any, any>, message: Message, emoji: Constants.APIEmoji, member: Member) {
|
async handle(context: CommandClient<any, any>, message: Message, emoji: Constants.APIEmoji, member: Member) {
|
||||||
const _config = await database.config.getOrCreateConfig(member.guild.id);
|
const _message = await context.getMessage(message.channel.id, message.id);
|
||||||
if (emoji.id !== _config.emoji_id) return;
|
if (!_message.guildID || emoji.id != '1547870117080342548' || member.id == _message.author.id) return;
|
||||||
|
|
||||||
const _entry = await database.points.getOrCreatePoints(member.guild.id, member.id);
|
await database`
|
||||||
await database.points.updatePoints(member.guild.id, member.id, _entry + 1n);
|
INSERT INTO points AS p (user_id, guild_id, points)
|
||||||
|
VALUES (${_message.author.id}, ${_message.guildID}, 1)
|
||||||
|
ON CONFLICT (user_id, guild_id)
|
||||||
|
DO UPDATE SET points = p.points + 1
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ class MessageReactionRemoveEvent extends Event<CommandClient> {
|
|||||||
event: string = 'messageReactionRemove' as const;
|
event: string = 'messageReactionRemove' as const;
|
||||||
|
|
||||||
async handle(context: CommandClient<any, any>, message: { id: string; channel: { id: string } }, emoji: Constants.APIEmoji, userId: string) {
|
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);
|
const _message = await context.getMessage(message.channel.id, message.id);
|
||||||
if (!_msg.guildID) return;
|
if (!_message.guildID || emoji.id != '1547870117080342548' || userId == _message.author.id) return;
|
||||||
|
|
||||||
const _config = await database.config.getOrCreateConfig(_msg.guildID);
|
await database`
|
||||||
if (emoji.id !== _config.emoji_id) return;
|
INSERT INTO points AS p (user_id, guild_id, points)
|
||||||
|
VALUES (${_message.author.id}, ${_message.guildID}, 0)
|
||||||
const _entry = await database.points.getOrCreatePoints(_msg.guildID, userId);
|
ON CONFLICT (user_id, guild_id)
|
||||||
await database.points.updatePoints(_msg.guildID, userId, _entry - 1n);
|
DO UPDATE SET points = GREATEST(p.points - 1, 0)
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -1,6 +1,6 @@
|
|||||||
import 'dotenv/config';
|
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 commands from './commands';
|
||||||
import database from './database';
|
import database from './database';
|
||||||
import events from './events';
|
import events from './events';
|
||||||
@@ -26,5 +26,16 @@ events.forEach((event) => client.registerEvent(event, true));
|
|||||||
|
|
||||||
// ----------
|
// ----------
|
||||||
|
|
||||||
database.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();
|
client.connect();
|
||||||
|
|||||||
+1
-1
@@ -8,5 +8,5 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "types/**/*.ts"],
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-6
@@ -1,14 +1,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace Application {
|
|
||||||
type Configuration = {
|
|
||||||
emoji_id: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
DISCORD_TOKEN: string;
|
DISCORD_TOKEN: string;
|
||||||
|
LOGGING_CHANNEL: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user