From c7c537665f10091efe08b2cddd8c61420743e54d Mon Sep 17 00:00:00 2001 From: ToastedToast Date: Sun, 14 Jul 2024 20:33:21 +0800 Subject: [PATCH] feat(updates): refactor --- api/db/init.ts | 20 ++------ api/db/queries/guilds.ts | 11 +++-- api/db/queries/updates.ts | 46 +++++++++---------- api/index.ts | 31 +++++++++---- bot/commands.ts | 83 ++++++++++++++++++++++++++++------ bot/events/messageCreate.ts | 4 +- bot/utils/handleLevelChange.ts | 24 ++-------- bot/utils/requestAPI.ts | 21 +++++++-- 8 files changed, 145 insertions(+), 95 deletions(-) diff --git a/api/db/init.ts b/api/db/init.ts index 7ae8380..2f67f22 100644 --- a/api/db/init.ts +++ b/api/db/init.ts @@ -7,7 +7,9 @@ export async function initTables() { name VARCHAR(255), icon VARCHAR(255), members INT, - cooldown INT DEFAULT 30000 + cooldown INT DEFAULT 30000, + updates_enabled BOOLEAN DEFAULT FALSE, + updates_channel_id VARCHAR(255) DEFAULT NULL ) `; const createUsersTable = ` @@ -34,14 +36,6 @@ export async function initTables() { ) `; // FOREIGN KEY (guild_id) REFERENCES guilds(id) - const createUpdatesTable = ` - CREATE TABLE IF NOT EXISTS updates ( - guild_id VARCHAR(255) NOT NULL PRIMARY KEY, - channel_id VARCHAR(255) NOT NULL, - enabled BOOLEAN DEFAULT FALSE - ) - `; - // FOREIGN KEY (guild_id) REFERENCES guilds(id) pool.query(createGuildsTable, (err) => { if (err) { @@ -66,12 +60,4 @@ export async function initTables() { console.log("Roles table created"); } }); - - pool.query(createUpdatesTable, (err) => { - if (err) { - console.error("Error creating updates table:", err); - } else { - console.log("Updates table created"); - } - }); } diff --git a/api/db/queries/guilds.ts b/api/db/queries/guilds.ts index c0eb8a7..d1f7bcb 100644 --- a/api/db/queries/guilds.ts +++ b/api/db/queries/guilds.ts @@ -7,8 +7,11 @@ export interface Guild { icon: string; members: number; cooldown: number; + updates_enabled: 0 | 1; + updates_channel_id: string | null; } + export async function getGuild(guildId: string): Promise<[QueryError, null] | [null, Guild | null]> { return new Promise((resolve, reject) => { pool.query("SELECT * FROM guilds WHERE id = ?", [guildId], (err, results) => { @@ -21,24 +24,22 @@ export async function getGuild(guildId: string): Promise<[QueryError, null] | [n }); } -export async function updateGuild(guild: Guild): Promise<[QueryError | null, null] | [null, Guild[]]> { +export async function updateGuild(guild: Omit): Promise<[QueryError | null, null] | [null, Guild[]]> { return new Promise((resolve, reject) => { pool.query( ` - INSERT INTO guilds (id, name, icon, members, cooldown) + INSERT INTO guilds (id, name, icon, members) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), icon = VALUES(icon), - members = VALUES(members), - cooldown = VALUES(cooldown) + members = VALUES(members) `, [ guild.id, guild.name, guild.icon, guild.members, - guild.cooldown ], (err, results) => { console.dir(results, { depth: null }); diff --git a/api/db/queries/updates.ts b/api/db/queries/updates.ts index 5fc33e0..80bfdbc 100644 --- a/api/db/queries/updates.ts +++ b/api/db/queries/updates.ts @@ -7,36 +7,38 @@ export interface Updates { enabled: boolean; } -export async function getUpdates(guildId: string): Promise<[QueryError | null, Updates[] | null]> { +export async function enableUpdates(guildId: string): Promise<[QueryError | null, boolean]> { return new Promise((resolve, reject) => { - pool.query("SELECT * FROM updates WHERE guild_id = ?", [guildId], (err, results) => { - if (err) { - reject([err, null]); - } else { - resolve([null, results as Updates[]]); - } - }); + pool.query( + ` + UPDATE guilds SET updates_enabled = TRUE WHERE id = ? + `, + [ + guildId, + ], + (err) => { + if (err) { + reject([err, false]); + } else { + resolve([null, true]); + } + }, + ); }); } -export async function enableUpdates(guildId: string, channelId: string): Promise<[QueryError | null, true | null]> { +export async function disableUpdates(guildId: string): Promise<[QueryError | null, boolean]> { return new Promise((resolve, reject) => { pool.query( ` - INSERT INTO updates (guild_id, channel_id, enabled) - VALUES (?, ?, ?) - ON DUPLICATE KEY UPDATE - channel_id = VALUES(channel_id), - enabled = VALUES(enabled) + UPDATE guilds SET updates_enabled = FALSE WHERE id = ? `, [ guildId, - channelId, - true, ], (err) => { if (err) { - reject([err, null]); + reject([err, false]); } else { resolve([null, true]); } @@ -45,21 +47,19 @@ export async function enableUpdates(guildId: string, channelId: string): Promise }); } -export async function disableUpdates(guildId: string): Promise<[QueryError | null, true | null]> { +export async function setUpdatesChannel(guildId: string, channelId: string | null): Promise<[QueryError | null, boolean]> { return new Promise((resolve, reject) => { pool.query( ` - UPDATE updates - SET enabled = ? - WHERE guild_id = ? + UPDATE guilds SET updates_channel_id = ? WHERE id = ? `, [ - false, + channelId, guildId, ], (err) => { if (err) { - reject([err, null]); + reject([err, false]); } else { resolve([null, true]); } diff --git a/api/index.ts b/api/index.ts index 46ee10b..63925d3 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,7 +1,7 @@ import express, { type NextFunction, type Request, type Response } from "express"; import cors from "cors"; import path from "path"; -import { getBotInfo, getGuild, getUser, getUsers, initTables, pool, updateGuild, getUpdates, enableUpdates, disableUpdates, setCooldown } from "./db"; +import { getBotInfo, getGuild, getUser, getUsers, initTables, pool, updateGuild, enableUpdates, disableUpdates, setCooldown, setUpdatesChannel } from "./db"; const app = express(); const PORT = 18103; @@ -34,7 +34,6 @@ app.post("/post/:guild", authMiddleware, async (req, res) => { name, icon, members, - cooldown: 30_000, }); if (err) { @@ -57,6 +56,7 @@ app.post("/post/:guild/:user", authMiddleware, async (req, res) => { return res.status(500).json({ message: "Internal server error" }); } + const currentXp = result?.xp ?? 0; const currentLevelSaved = result?.level ?? 0; const newXp = currentXp + xpValue; @@ -168,15 +168,12 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => { // run function to exclude target from guild break; case "updates": - if (target !== "enable" && target !== "disable" && target !== "get") { + if (target !== "enable" && target !== "disable" && target !== "set" && target !== "get") { return res.status(400).json({ message: "Illegal request" }); } switch (target) { case "enable": - if (!extraData || !extraData.channelId) { - return res.status(400).json({ message: "Illegal request" }); - } try { const [err, success] = await enableUpdates(guild, extraData.channelId); if (err) { @@ -198,13 +195,31 @@ app.post("/admin/:action/:guild/:target", authMiddleware, async (req, res) => { } catch (err) { return res.status(500).json({ message: "Internal server error", err }); } + case 'set': + if (!extraData || typeof extraData.channelId === "undefined") { + return res.status(400).json({ message: "Illegal request" }); + } + + try { + const [err, success] = await setUpdatesChannel(guild, extraData.channelId); + if (err) { + return res.status(500).json({ message: 'Internal server error', err }); + } else { + return res.status(200).json(success); + } + } catch (err) { + return res.status(500).json({ message: 'Internal server error', err }); + } default: try { - const [err, data] = await getUpdates(guild); + const [err, data] = await getGuild(guild); if (err) { return res.status(500).json({ message: "Internal server error", err }); } - return res.status(200).json(data); + return res.status(200).json({ + enabled: ((data?.updates_enabled ?? 1) === 1), + channel: data?.updates_channel_id ?? null, + }); } catch (error) { return res.status(500).json({ message: "Internal server error" }); } diff --git a/bot/commands.ts b/bot/commands.ts index 475dc4c..94bff3b 100644 --- a/bot/commands.ts +++ b/bot/commands.ts @@ -3,7 +3,7 @@ import client from '.'; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type CommandInteraction, ChannelType, type APIApplicationCommandOption, GuildMember, AttachmentBuilder, ComponentType } from 'discord.js'; import { heapStats } from 'bun:jsc'; -import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, checkIfGuildHasUpdatesEnabled } from './utils/requestAPI'; +import { getGuildLeaderboard, makeGETRequest, getRoles, removeRole, addRole, enableUpdates, disableUpdates, getCooldown, setCooldown, getUpdatesChannel, setUpdatesChannel } from './utils/requestAPI'; import convertToLevels from './utils/convertToLevels'; import quickEmbed from './utils/quickEmbed'; import { Font, RankCardBuilder } from 'canvacord'; @@ -161,7 +161,7 @@ const commands: Record = { .setDisplayName(member.displayName) .setAvatar(member.displayAvatarURL({ forceStatic: true, size: 4096 })) // user avatar .setCurrentXP(xp.xp) // current xp - .setRequiredXP(xp.xp_needed_next_level) // required xp + .setRequiredXP(xp.xp + xp.xp_needed_next_level) // required xp .setLevel(xp.level) // user level .setRank(rank) // user rank .setOverlay(member.user.banner ? 95 : 90) // overlay percentage. Overlay is a semi-transparent layer on top of the background @@ -450,24 +450,37 @@ const commands: Record = { data: { options: [{ name: 'action', - description: 'Note that enabling is in THIS channel and will override the current updates channel!', + description: 'Select an action', type: 3, required: true, choices: [ { - name: 'check', + name: 'Check', value: 'check', }, { - name: 'enable', + name: 'Enable', value: 'enable', }, { - name: 'disable', + name: 'Disable', value: 'disable', - } + }, + { + name: 'Set', + value: 'set', + }, + { + name: 'Reset to Default', + value: 'reset', + }, ] - },], + },{ + name: 'channel', + description: 'Enter the channel ID. Required for set action.', + type: 7, + required: false, + }], name: 'updates', description: 'Get the latest updates on the bot!', integration_types: [0], @@ -494,6 +507,14 @@ const commands: Record = { let data switch (action) { + case 'enable': + success = await enableUpdates(interaction.guildId as string); + if (!success) { + await interaction.reply({ ephemeral: true, content: 'Error enabling updates for this server' }).catch(console.error); + return; + } + await interaction.reply({ ephemeral: true, content: `Updates are now enabled for this server` }).catch(console.error); + return; case 'disable': success = await disableUpdates(interaction.guildId as string); if (!success) { @@ -502,22 +523,54 @@ const commands: Record = { } await interaction.reply({ ephemeral: true, content: 'Updates are now disabled for this server' }).catch(console.error); return; - case 'enable': - success = await enableUpdates(interaction.guildId as string, channelId as string); + case 'set': + if(!channelId) { + await interaction.reply({ ephemeral: true, content: 'ERROR: Channel was not specified!' }); + return; + } + success = await setUpdatesChannel(interaction.guildId as string, channelId); if (!success) { - await interaction.reply({ ephemeral: true, content: 'Error enabling updates for this server' }).catch(console.error); + await interaction.reply({ ephemeral: true, content: 'Error setting updates channel for this server' }).catch(console.error); return; } - await interaction.reply({ ephemeral: true, content: `Updates are now enabled for this server in <#${channelId}>` }).catch(console.error); + await interaction.reply({ ephemeral: true, content: `Updates channel has been set to <#${channelId}>` }).catch(console.error); return; + case 'reset': + success = await setUpdatesChannel(interaction.guildId as string, null); + if (!success) { + await interaction.reply({ ephemeral: true, content: 'Error resetting updates channel for this server' }).catch(console.error); + return; + } + await interaction.reply({ ephemeral: true, content: `Updates channel has been reset to default` }).catch(console.error); + return default: - data = await checkIfGuildHasUpdatesEnabled(interaction.guildId as string); + data = await getUpdatesChannel(interaction.guildId as string); if (!data || Object.keys(data).length === 0) { await interaction.reply({ ephemeral: true, content: 'No data found' }).catch(console.error); return; } - // TODO: Format in embed - await interaction.reply({ ephemeral: true, content: JSON.stringify(data, null, 2) }).catch(console.error); + await interaction.reply({ + embeds: [ + quickEmbed({ + color: 'Blurple', + title: 'Updates', + description: 'Updates for this server', + }, interaction) + .addFields( + { + name: 'Enabled', + value: data.enabled ? 'Yes' : 'No', + inline: true, + }, + { + name: 'Channel', + value: data.channel ? `<#${data.channel}>` : 'N/A', + inline: true, + }, + ) + ], + ephemeral: true + }).catch(console.error); return; } }, diff --git a/bot/events/messageCreate.ts b/bot/events/messageCreate.ts index e3dafa6..1309d32 100644 --- a/bot/events/messageCreate.ts +++ b/bot/events/messageCreate.ts @@ -9,7 +9,7 @@ client.on('messageCreate', async (message: Message) => { if (message.author.bot) return; const cooldownTime = (await getCooldown(message.guildId as string))?.cooldown ?? 30_000; - + const cooldown = cooldowns.get(message.author.id); if (cooldown && Date.now() - cooldown < cooldownTime) return; @@ -17,7 +17,7 @@ client.on('messageCreate', async (message: Message) => { const pfp: string = message.member?.displayAvatarURL() ?? message.author.displayAvatarURL() const name: string = message.author.username; const nickname: string = message.member?.nickname ?? message.author.globalName ?? message.author.username; - await makePOSTRequest(message.guildId as string, message.author.id, xpToGive, pfp, name, nickname); + await makePOSTRequest(message.guildId as string, message.author.id, message.channel.id, xpToGive, pfp, name, nickname); cooldowns.set(message.author.id, Date.now()); const guildName = message.guild?.name; diff --git a/bot/utils/handleLevelChange.ts b/bot/utils/handleLevelChange.ts index b110ddc..6033330 100644 --- a/bot/utils/handleLevelChange.ts +++ b/bot/utils/handleLevelChange.ts @@ -1,30 +1,14 @@ // import quickEmbed from "./quickEmbed"; import type { TextChannel } from "discord.js"; import client from ".."; +import { getUpdatesChannel } from "./requestAPI"; -export default async function(guild: string, user: string, level: number) { - const hasUpdates = await checkIfGuildHasUpdatesEnabled(guild); +export default async function(guild: string, user: string, channelId: string, level: number) { + const hasUpdates = await getUpdatesChannel(guild); if (!hasUpdates.enabled) return; - const channel = await client.channels.fetch(hasUpdates.channelId) as TextChannel; + const channel = await client.channels.fetch(hasUpdates.channelId ?? channelId) as TextChannel; if (channel) { channel.send(`<@${user}> has reached level ${level}!`); } } - -export async function checkIfGuildHasUpdatesEnabled(guild: string): Promise<{ enabled: boolean, channelId: string }> { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/get`, { - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ auth: process.env.AUTH }), - method: 'POST', - }); - - const data = await response.json(); - - return { - enabled: data[0].enabled === 1, - channelId: data[0].channel_id, - }; -} \ No newline at end of file diff --git a/bot/utils/requestAPI.ts b/bot/utils/requestAPI.ts index cae942a..c497a00 100644 --- a/bot/utils/requestAPI.ts +++ b/bot/utils/requestAPI.ts @@ -1,6 +1,6 @@ import handleLevelChange from "./handleLevelChange"; -export async function makePOSTRequest(guild: string, user: string, xp: number, pfp: string, name: string, nickname: string) { +export async function makePOSTRequest(guild: string, user: string, channel: string, xp: number, pfp: string, name: string, nickname: string) { await fetch(`http://localhost:18103/post/${guild}/${user}`, { headers: { 'Content-Type': 'application/json', @@ -11,7 +11,7 @@ export async function makePOSTRequest(guild: string, user: string, xp: number, p }).then(res => { return res.json() }).then(data => { - if (data.sendUpdateEvent) handleLevelChange(guild, user, data.level) + if (data.sendUpdateEvent) handleLevelChange(guild, user, channel, data.level) }) } @@ -110,7 +110,7 @@ export async function addRole(guild: string, role: string, level: number): Promi //#endregion //#region Updates -export async function checkIfGuildHasUpdatesEnabled(guild: string) { +export async function getUpdatesChannel(guild: string) { const response = await fetch(`http://localhost:18103/admin/updates/${guild}/get`, { "headers": { 'Content-Type': 'application/json', @@ -121,8 +121,8 @@ export async function checkIfGuildHasUpdatesEnabled(guild: string) { }); return response.status === 200 ? response.json() : {}; } -export async function enableUpdates(guild: string, channelId: string) { - const response = await fetch(`http://localhost:18103/admin/updates/${guild}/enable`, { +export async function setUpdatesChannel(guild: string, channelId: string | null) { + const response = await fetch(`http://localhost:18103/admin/updates/${guild}/set`, { "headers": { 'Content-Type': 'application/json', 'Authorization': process.env.AUTH as string, @@ -132,6 +132,17 @@ export async function enableUpdates(guild: string, channelId: string) { }); return response.status === 200; } +export async function enableUpdates(guild: string) { + const response = await fetch(`http://localhost:18103/admin/updates/${guild}/enable`, { + "headers": { + 'Content-Type': 'application/json', + 'Authorization': process.env.AUTH as string, + }, + "body": JSON.stringify({}), + "method": "POST" + }); + return response.status === 200; +} export async function disableUpdates(guild: string) { const response = await fetch(`http://localhost:18103/admin/updates/${guild}/disable`, { "headers": {