From 32b06a384ba1c184fbd905830067ef8e14e0f8d3 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 10:27:07 +0800 Subject: [PATCH 01/14] (rewrite): Bot & Events. This most definitely doesn't work yet. I'm just running out of time to work on this atm. --- .npmrc | 1 + package-lock.json | 93 ++++++++++++++++++- package.json | 4 + src/bot.ts | 32 +++++++ src/commands/private-help.ts | 2 +- src/commands/settings.ts | 28 +++--- src/config.ts | 15 +-- src/events/guildBanRemove.ts | 17 ++++ src/events/guildCreate.ts | 15 +++ src/events/guildMemberAdd.ts | 24 +++++ src/events/handle-ban-remove.ts | 6 -- src/events/handle-guild-create.ts | 10 -- src/events/handle-member-join.ts | 23 ----- src/events/index.ts | 8 +- src/scheduler/index.ts | 13 +-- .../tasks/check-expired-bans-task.ts | 9 +- .../tasks/close-stale-tickets-task.ts | 9 +- .../tasks/prune-old-messages-task.ts | 6 +- src/shared/Database.ts | 27 +++--- src/types/database-types.d.ts | 1 - src/types/scheduler.d.ts | 4 +- src/utils/alt-utils.ts | 28 ++---- src/utils/ban-utils.ts | 22 ++--- src/utils/embed-utils.ts | 18 ++-- src/utils/private-help-utils.ts | 31 +++---- 25 files changed, 292 insertions(+), 154 deletions(-) create mode 100644 src/bot.ts create mode 100644 src/events/guildBanRemove.ts create mode 100644 src/events/guildCreate.ts create mode 100644 src/events/guildMemberAdd.ts delete mode 100644 src/events/handle-ban-remove.ts delete mode 100644 src/events/handle-guild-create.ts delete mode 100644 src/events/handle-member-join.ts diff --git a/.npmrc b/.npmrc index 7eec312..8e6e757 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +registry=https://npm.hep.gg @clynamic:registry=https://npm.clynamic.net diff --git a/package-lock.json b/package-lock.json index b0434ce..ae41f49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { - "name": "discordbot-ng", + "name": "hexerade", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "discordbot-ng", "dependencies": { "@clynamic/dmark": "0.0.11", "@redis/client": "5.1.0", + "athena-prime": "4.2.0", "body-parser": "2.2.2", "discord.js": "14.26.4", "dotenv": "16.5.0", "express": "5.1.0", "express-session": "1.19.0", + "ioredis": "6.0.0", "markdown-it": "14.2.0", "memorystore": "1.6.7", "sqlite": "5.1.1", @@ -824,6 +825,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ioredis/commands": { + "version": "2.0.0", + "resolved": "https://npm.hep.gg/@ioredis/commands/-/commands-2.0.0.tgz", + "integrity": "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1394,6 +1401,22 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/athena-prime": { + "version": "4.2.0", + "resolved": "https://npm.hep.gg/athena-prime/-/athena-prime-4.2.0.tgz", + "integrity": "sha512-CbIPwdupVGfGRR/ZRcc9BAC/nBnRQS9sRalzWtONYQ4eNhCDJVykPLQM2N7abndvcpLctWd1cD3awpA9NDy30Q==", + "license": "NOLICENSE", + "dependencies": { + "discord-api-types": "^0.38.48", + "eventemitter3": "^5.0.1" + }, + "engines": { + "node": ">=22.15.0" + }, + "optionalDependencies": { + "ws": "^8.21.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1821,6 +1844,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://npm.hep.gg/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1838,9 +1870,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.42", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", - "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", + "version": "0.38.53", + "resolved": "https://npm.hep.gg/discord-api-types/-/discord-api-types-0.38.53.tgz", + "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -2203,6 +2235,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://npm.hep.gg/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2761,6 +2799,36 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/ioredis": { + "version": "6.0.0", + "resolved": "https://npm.hep.gg/ioredis/-/ioredis-6.0.0.tgz", + "integrity": "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "2.0.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://npm.hep.gg/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3679,6 +3747,15 @@ "node": ">= 6" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://npm.hep.gg/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4124,6 +4201,12 @@ } } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://npm.hep.gg/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index bcfafe5..536de8d 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ "typescript": "5.8.3", "typescript-eslint": "8.32.1" }, + "dependencies": { "@clynamic/dmark": "0.0.11", "@redis/client": "5.1.0", + "ioredis": "6.0.0", + "athena-prime": "4.2.0", "body-parser": "2.2.2", "discord.js": "14.26.4", "dotenv": "16.5.0", @@ -24,6 +27,7 @@ "sqlite": "5.1.1", "sqlite3": "6.0.1" }, + "scripts": { "dev": "tsx watch --enable-source-maps src/index.ts", "start": "node dist/index.js", diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..a8fe299 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,32 @@ +// External Imports +import { CommandClient, Constants, LRUCollection, Member, User } from "athena-prime"; + +// Internal Imports +import config from "./config"; + +// --------------- + +const client = new CommandClient({ + token: `Bot ${config.DISCORD_TOKEN}`, + + options: { + intents: [ + Constants.GatewayIntentBits.Guilds, + Constants.GatewayIntentBits.GuildMembers, + Constants.GatewayIntentBits.GuildMessages, + Constants.GatewayIntentBits.GuildModeration, + Constants.GatewayIntentBits.GuildVoiceStates, + Constants.GatewayIntentBits.MessageContent, + ], + + largeBotOptimizations: true, + + // TODO: Move caching to Redis. + cache: { + users: () => new LRUCollection(User, 1024), + members: (guild) => new LRUCollection(Member, 1024), + } + } +}); + +export default client; \ No newline at end of file diff --git a/src/commands/private-help.ts b/src/commands/private-help.ts index 1f77610..d3a18e2 100644 --- a/src/commands/private-help.ts +++ b/src/commands/private-help.ts @@ -39,7 +39,7 @@ export default { .addComponents(button); await interaction.channel.send({ components: [row], content }); - await Database.updateGuildSettings(interaction.guildId, 'private_help_channel_id', interaction.channelId); + await Database.updateSettings(interaction.guildId, 'private_help_channel_id', interaction.channelId); interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' }); } }; diff --git a/src/commands/settings.ts b/src/commands/settings.ts index afc5f9e..7bfdc9d 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -139,75 +139,75 @@ export default { const generalChannel = interaction.options.getChannel('general-channel'); if (generalChannel) { - await Database.updateGuildSettings(interaction.guildId, 'general_chat_id', generalChannel.id); + await Database.updateSettings(interaction.guildId, 'general_chat_id', generalChannel.id); response.push(`**general_chat_id** has been set to: ${generalChannel}.`); } const ticketsChannel = interaction.options.getChannel('tickets-channel'); if (ticketsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'tickets_channel_id', ticketsChannel.id); + await Database.updateSettings(interaction.guildId, 'tickets_channel_id', ticketsChannel.id); response.push(`**tickets_channel_id** has been set to: ${ticketsChannel}.`); } const appealsChannel = interaction.options.getChannel('appeals-channel'); if (appealsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'appeals_channel_id', appealsChannel.id); + await Database.updateSettings(interaction.guildId, 'appeals_channel_id', appealsChannel.id); response.push(`**appeals_channel_id** has been set to ${appealsChannel}.`); } const eventLogsChannel = interaction.options.getChannel('event-logs-channel'); if (eventLogsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id); + await Database.updateSettings(interaction.guildId, 'event_logs_channel_id', eventLogsChannel.id); response.push(`**event_logs_channel_id** has been set to: ${eventLogsChannel}.`); } const discordLogsChannel = interaction.options.getChannel('discord-logs-channel'); if (discordLogsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'discord_logs_channel_id', discordLogsChannel.id); + await Database.updateSettings(interaction.guildId, 'discord_logs_channel_id', discordLogsChannel.id); response.push(`**discord_logs_channel_id** has been set to: ${discordLogsChannel}.`); } const auditLogsChannel = interaction.options.getChannel('audit-logs-channel'); if (auditLogsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'audit_logs_channel_id', auditLogsChannel.id); + await Database.updateSettings(interaction.guildId, 'audit_logs_channel_id', auditLogsChannel.id); response.push(`**audit_logs_channel_id** has been set to: ${auditLogsChannel}.`); } const voiceLogsChannel = interaction.options.getChannel('voice-logs-channel'); if (voiceLogsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'voice_logs_channel_id', voiceLogsChannel.id); + await Database.updateSettings(interaction.guildId, 'voice_logs_channel_id', voiceLogsChannel.id); response.push(`**voice_logs_channel_id** has been set to: ${voiceLogsChannel}.`); } const newMemberLogsChannel = interaction.options.getChannel('new-member-channel'); if (newMemberLogsChannel) { - await Database.updateGuildSettings(interaction.guildId, 'new_member_channel_id', newMemberLogsChannel.id); + await Database.updateSettings(interaction.guildId, 'new_member_channel_id', newMemberLogsChannel.id); response.push(`**new_member_channel_id** has been set to: ${newMemberLogsChannel}.`); } const moderatorChannel = interaction.options.getChannel('moderator-channel'); if (moderatorChannel) { - await Database.updateGuildSettings(interaction.guildId, 'moderator_channel_id', moderatorChannel.id); + await Database.updateSettings(interaction.guildId, 'moderator_channel_id', moderatorChannel.id); response.push(`**moderator_channel_id** has been set to": ${moderatorChannel}.`); } const adminRole = interaction.options.getRole('admin-role'); if (adminRole) { - await Database.updateGuildSettings(interaction.guildId, 'admin_role_id', adminRole.id); + await Database.updateSettings(interaction.guildId, 'admin_role_id', adminRole.id); response.push(`**admin_role_id** has been set to: ${adminRole}.`); } const privateHelperRole = interaction.options.getRole('private-helper-role'); if (privateHelperRole) { - await Database.updateGuildSettings(interaction.guildId, 'private_help_role_id', privateHelperRole.id); + await Database.updateSettings(interaction.guildId, 'private_help_role_id', privateHelperRole.id); response.push(`**private_help_role_id** has been set to: ${privateHelperRole}.`); } const devWatchRole = interaction.options.getRole('devwatch-role'); if (devWatchRole) { - await Database.updateGuildSettings(interaction.guildId, 'devwatch_role_id', devWatchRole.id); + await Database.updateSettings(interaction.guildId, 'devwatch_role_id', devWatchRole.id); response.push(`**devwatch_role_id** has been set to ${devWatchRole}.`); } @@ -264,13 +264,13 @@ export default { const githubReleaseChannel = interaction.options.getChannel('github-release-channel'); if (githubReleaseChannel) { - await Database.updateGuildSettings(interaction.guildId, 'github_release_channel', githubReleaseChannel.id); + await Database.updateSettings(interaction.guildId, 'github_release_channel', githubReleaseChannel.id); response.push(`**github_release_channel** has been set to ${githubReleaseChannel}.`); } const siteBreakerRole = interaction.options.getRole('site-breaker-role'); if (siteBreakerRole) { - await Database.updateGuildSettings(interaction.guildId, 'site_breaker_role_id', siteBreakerRole.id); + await Database.updateSettings(interaction.guildId, 'site_breaker_role_id', siteBreakerRole.id); response.push(`**site_breaker_role_id** has been set to ${siteBreakerRole}.`); } diff --git a/src/config.ts b/src/config.ts index 670b7b9..55d8917 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,11 @@ -import dotenv from 'dotenv'; +// External Imports +import 'dotenv/config'; -dotenv.config(); +// --------------- const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_GUILD_ID, RELEASE_SECRET, DATABASE_SECRET, LINK_SECRET, E621_BASE_URL, E926_BASE_URL, PRIVACY_POLICY_URL, GIT_REPO_BASE_URL, REDIS_URL, PORT, DEBUG } = process.env; -export const config = { +export default { DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, @@ -20,10 +21,4 @@ export const config = { REDIS_URL, DEV_MODE: process.env.npm_lifecycle_event == 'dev', DEBUG: DEBUG == 'true' -}; - -for (const [key, val] of Object.entries(config)) { - if (val === undefined) { - throw new Error(`${key} is undefined in config`); - } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/events/guildBanRemove.ts b/src/events/guildBanRemove.ts new file mode 100644 index 0000000..1a3c523 --- /dev/null +++ b/src/events/guildBanRemove.ts @@ -0,0 +1,17 @@ +// External Imports +import { CommandClient, Event, Guild, User } from 'athena-prime'; + +// Internal Imports +import { Database } from '../shared/Database'; + +// --------------- + +class GuildBanRemoveEvent extends Event { + event: string = 'guildBanRemove' as const; + + async handle(context: CommandClient, guild: Guild, user: User) { + await Database.deleteBan(user.id); + } +} + +export default new GuildBanRemoveEvent('guildBanRemove'); \ No newline at end of file diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts new file mode 100644 index 0000000..df1e16c --- /dev/null +++ b/src/events/guildCreate.ts @@ -0,0 +1,15 @@ +// External Imports +import { CommandClient, Event, Guild } from 'athena-prime'; + +// Internal Imports +import { Database } from '../shared/Database'; + +class GuildCreateEvent extends Event { + event: string = 'guildCreate' as const; + + async handle(context: CommandClient, guild: Guild) { + await Database.GetOrCreateSettings(guild.id); + } +} + +export default new GuildCreateEvent('guildCreate'); \ No newline at end of file diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts new file mode 100644 index 0000000..41d5b6f --- /dev/null +++ b/src/events/guildMemberAdd.ts @@ -0,0 +1,24 @@ +// External Imports +import { CommandClient, Event, Guild, Member } from 'athena-prime'; + +// Internal Imports +import { Database } from '../shared/Database'; +import { getE621Alts } from '../utils'; + +// --------------- + +class GuildMemberAddEvent extends Event { + event: string = 'guildMemberAdd' as const; + + async handle(context: CommandClient, guild: Guild, member: Member) { + const settings = await Database.GetOrCreateSettings(guild.id); + + // TODO: Seperate this into its own function and create a handleWelcomeMessage() function. + const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`; + + if (!settings.moderator_channel_id || !content.includes('[BANNED]')) return; + context.createMessage(settings.moderator_channel_id, content); + } +} + +export default new GuildMemberAddEvent('guildMemberAdd'); \ No newline at end of file diff --git a/src/events/handle-ban-remove.ts b/src/events/handle-ban-remove.ts deleted file mode 100644 index edc9888..0000000 --- a/src/events/handle-ban-remove.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { GuildBan } from 'discord.js'; -import { Database } from '../shared/Database'; - -export async function handleBanRemove(ban: GuildBan) { - await Database.removeBan(ban.user.id); -} \ No newline at end of file diff --git a/src/events/handle-guild-create.ts b/src/events/handle-guild-create.ts deleted file mode 100644 index 65ebc27..0000000 --- a/src/events/handle-guild-create.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Guild } from 'discord.js'; -import { Database } from '../shared/Database'; - -export async function handleGuildCreate(guild: Guild) { - try { - if (!await Database.getGuildSettings(guild.id)) await Database.putGuild(guild.id); - } catch (e) { - console.error(e); - } -} \ No newline at end of file diff --git a/src/events/handle-member-join.ts b/src/events/handle-member-join.ts deleted file mode 100644 index b625bef..0000000 --- a/src/events/handle-member-join.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { GuildMember, GuildTextBasedChannel } from 'discord.js'; -import { Database } from '../shared/Database'; -import { getE621Alts } from '../utils'; - -export async function handleMemberJoin(member: GuildMember) { - const guildSettings = await Database.getGuildSettings(member.guild.id); - - if (guildSettings?.new_member_channel_id) { - const channel = await member.guild.channels.fetch(guildSettings.new_member_channel_id) as GuildTextBasedChannel; - - if (channel) { - const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`; - - channel.send(content).catch(console.error); - - if (guildSettings.moderator_channel_id && content.includes('[BANNED]')) { - const modChannel = await member.guild.channels.fetch(guildSettings.moderator_channel_id) as GuildTextBasedChannel; - - if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error); - } - } - } -} \ No newline at end of file diff --git a/src/events/index.ts b/src/events/index.ts index dca7ff2..72d583f 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,7 +1,9 @@ export * from './handle-audit-log-create'; -export * from './handle-ban-remove'; -export * from './handle-guild-create'; -export * from './handle-member-join'; +import guildBanRemove from './guildBanRemove'; +import guildCreate from './guildCreate'; +import guildMemberAdd from './guildMemberAdd'; export * from './handle-message'; export * from './handle-thread-create'; export * from './handle-voice-state-update'; + +export default [guildBanRemove, guildCreate, guildMemberAdd]; \ No newline at end of file diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts index 22059df..6412e84 100644 --- a/src/scheduler/index.ts +++ b/src/scheduler/index.ts @@ -1,12 +1,15 @@ -import { Client } from 'discord.js'; +// External Imports +import { CommandClient } from 'athena-prime'; + +// Internal Imports import { Task } from '../types'; import ScheduledTasks from './tasks'; class Scheduler { private tasks = new Map(); - private context: Client; + private context: CommandClient; - constructor(context: Client) { + constructor(context: CommandClient) { this.context = context; } @@ -18,6 +21,4 @@ class Scheduler { } } -export { - ScheduledTasks, Scheduler -}; +export { Scheduler, ScheduledTasks } \ No newline at end of file diff --git a/src/scheduler/tasks/check-expired-bans-task.ts b/src/scheduler/tasks/check-expired-bans-task.ts index a6f8d0b..7e8122b 100644 --- a/src/scheduler/tasks/check-expired-bans-task.ts +++ b/src/scheduler/tasks/check-expired-bans-task.ts @@ -1,12 +1,17 @@ -import { Client } from 'discord.js'; +// External Imports +import { CommandClient } from 'athena-prime'; + +// Internal Imports import { Task } from '../../types'; import { checkExpiredBans } from '../../utils'; +// --------------- + class CheckExpiredBansTask implements Task { interval: number = 300000; firstRun: boolean = true; - async handle(context: Client): Promise { + async handle(context: CommandClient) { await checkExpiredBans(context); } } diff --git a/src/scheduler/tasks/close-stale-tickets-task.ts b/src/scheduler/tasks/close-stale-tickets-task.ts index a20022f..3d580e5 100644 --- a/src/scheduler/tasks/close-stale-tickets-task.ts +++ b/src/scheduler/tasks/close-stale-tickets-task.ts @@ -1,12 +1,17 @@ -import { Client } from 'discord.js'; +// External Imports +import { CommandClient } from 'athena-prime'; + +// Internal Imports import { Task } from '../../types'; import { closeOldTickets } from '../../utils'; +// --------------- + class CloseStaleTicketsTask implements Task { interval: number = 3.6e6; firstRun: boolean = true; - async handle(context: Client): Promise { + async handle(context: CommandClient) { await closeOldTickets(context); } } diff --git a/src/scheduler/tasks/prune-old-messages-task.ts b/src/scheduler/tasks/prune-old-messages-task.ts index 822a52e..eb0bbb2 100644 --- a/src/scheduler/tasks/prune-old-messages-task.ts +++ b/src/scheduler/tasks/prune-old-messages-task.ts @@ -1,3 +1,7 @@ +// External Imports +import { CommandClient } from 'athena-prime'; + +// Internal Imports import { Task } from '../../types'; import { Database } from '../../shared/Database'; @@ -5,7 +9,7 @@ class PruneOldMessagesTask implements Task { interval: number = 3.6e6; firstRun: boolean = true; - async handle(): Promise { + async handle(context: CommandClient) { await Database.pruneOldMessages(); } } diff --git a/src/shared/Database.ts b/src/shared/Database.ts index 49085c4..8722314 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -2,7 +2,7 @@ import path from 'path'; import { open, Database as SqliteDatabase } from 'sqlite'; import sqlite3 from 'sqlite3'; import { Message } from '../events'; -import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, RoleButton, TicketMessage, TicketPhrase } from '../types'; +import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, RoleButton, TicketMessage, TicketPhrase } from '../types'; import { deserializeMessage, serializeMessage, wait } from '../utils'; import { readFileSync } from 'fs'; import { Encrypter } from './Encrypter'; @@ -13,7 +13,7 @@ export const enum PrivateHelpTicketStatus { } export class Database { - private static db: SqliteDatabase; + static db: SqliteDatabase; static async open(file: string): Promise { if (Database.db) return; @@ -28,7 +28,6 @@ export class Database { console.log('SQLite database opened'); await Database.ensure(); - await Database.migrate(); } @@ -75,15 +74,19 @@ export class Database { //#region Guild Settings - static async getGuildSettings(guildId: string): Promise { - return await Database.db.get('SELECT * FROM settings WHERE guild_id = ?', guildId) as GuildSettings; - } + static async GetOrCreateSettings(guildId: string): Promise { + await Database.db.run( + 'INSERT OR IGNORE INTO settings (guild_id) VALUES (?)', + guildId + ); - static async putGuild(guildId: string) { - await Database.db.run('INSERT INTO settings(guild_id) VALUES (?)', guildId); - } + return await Database.db.get( + 'SELECT * FROM settings WHERE guild_id = ?', + guildId + ) as GuildSettings; +} - static async updateGuildSettings(guildId: string, key: GuildSetting, value: string) { + static async updateSettings(guildId: string, key: Exclude, value: string) { await Database.db.run(`UPDATE settings SET ${key} = ? WHERE guild_id = ?`, value, guildId); } @@ -332,10 +335,10 @@ export class Database { } static async pruneExpiredBans(date: Date) { - await Database.db.all('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date); + } - static async removeBan(userId: string) { + static async deleteBan(userId: string) { await Database.db.run('DELETE from bans WHERE user_id_hash = ?', Encrypter.hash(userId)); } diff --git a/src/types/database-types.d.ts b/src/types/database-types.d.ts index 4cf1097..eedc81c 100644 --- a/src/types/database-types.d.ts +++ b/src/types/database-types.d.ts @@ -33,7 +33,6 @@ export type GuildSettings = { site_breaker_role_id?: string } -type GuildSetting = Exclude export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels'; export type TicketMessage = { diff --git a/src/types/scheduler.d.ts b/src/types/scheduler.d.ts index 3a05cc0..0de62bd 100644 --- a/src/types/scheduler.d.ts +++ b/src/types/scheduler.d.ts @@ -1,8 +1,8 @@ -import { Client } from 'discord.js'; +import { CommandClient } from 'athena-prime'; export interface Task { interval: number; firstRun: boolean; - handle(context: Client): Promise; + handle(context: CommandClient): Promise; }; diff --git a/src/utils/alt-utils.ts b/src/utils/alt-utils.ts index 61d79f4..7d0141e 100644 --- a/src/utils/alt-utils.ts +++ b/src/utils/alt-utils.ts @@ -1,7 +1,10 @@ -import { Guild } from 'discord.js'; -import { Database } from '../shared/Database'; +// External Imports +import { Guild } from 'athena-prime'; + +// Internal Imports import { userIsBanned } from './e621-utils'; -import { config } from '../config'; +import config from '../config'; +import { Database } from '../shared/Database'; export type AltData = { type: 'e621' | 'discord' @@ -13,7 +16,6 @@ export type AltData = { export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ignore: number[] = []): Promise { const e621UserIds = await Database.getE621Ids(discordId); - const toIgnore = ignore.concat(e621UserIds); let content = ''; @@ -22,7 +24,6 @@ export async function getE621Alts(discordId: string, guild: Guild, depth = 1, ig if (ignore.includes(e621Id)) continue; const alts = await getDiscordAlts(e621Id, guild, depth + 1, toIgnore); - const banned = await userIsBanned(e621Id); content += `${' '.repeat((depth - 1) * 2)}- ${config.E621_BASE_URL}/users/${e621Id}${banned ? ' [BANNED]' : ''}\n${alts}`; @@ -38,13 +39,10 @@ export async function getDiscordAlts(e621Id: number, guild: Guild, depth = 1, ig for (const discordId of discordIds) { const alts = await getE621Alts(discordId, guild, depth + 1, ignore); - let banned = false; // It's either this or fetch all the bans and sift through them for every discord alt. - try { - banned = !!(await guild.bans.fetch(discordId)); - } catch (e) { } + banned = !!(await guild.getBan(discordId)); content += `${' '.repeat((depth - 1) * 2)}- <@${discordId}> (${discordId})${banned ? ' [BANNED]' : ''}\n${alts}`; } @@ -69,14 +67,13 @@ async function getE621AltData(discordId: string, guild: Guild | null, depth = 1, // It's either this or fetch all the bans and sift through them for every discord alt. try { - banned = guild ? !!(await guild.bans.fetch(discordId)) : false; + banned = guild ? !!(await guild.getBan(discordId)) : false; } catch (e) { } const data: AltData = { type: 'discord', thisId: discordId, banned, alts: [] }; for (const e621Id of e621UserIds) { if (ignore.includes(e621Id)) continue; - data.alts.push(await getDiscordAltData(e621Id, guild, depth + 1, toIgnore)); } @@ -85,12 +82,9 @@ async function getE621AltData(discordId: string, guild: Guild | null, depth = 1, async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, ignore: number[] = []): Promise { const discordIds = await Database.getDiscordIds(e621Id); - const data: AltData = { type: 'e621', thisId: e621Id, banned: await userIsBanned(e621Id), alts: [] }; - for (const discordId of discordIds) { - data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore)); - } + for (const discordId of discordIds) data.alts.push(await getE621AltData(discordId, guild, depth + 1, ignore)); return data; } @@ -98,9 +92,7 @@ async function getDiscordAltData(e621Id: number, guild: Guild | null, depth = 1, export function e621IdsFromAltData(altData: AltData, data: number[] = []) { if (altData.type == 'e621' && !data.includes(altData.thisId as number)) data.push(altData.thisId as number); - for (const alt of altData.alts) { - e621IdsFromAltData(alt, data); - } + for (const alt of altData.alts) e621IdsFromAltData(alt, data); return data; } \ No newline at end of file diff --git a/src/utils/ban-utils.ts b/src/utils/ban-utils.ts index 1ed7464..afba657 100644 --- a/src/utils/ban-utils.ts +++ b/src/utils/ban-utils.ts @@ -1,22 +1,22 @@ -import { Client } from 'discord.js'; +// External Imports +import { CommandClient } from 'athena-prime'; + +// Internal Imports +import config from '../config'; import { Database } from '../shared/Database'; -import { config } from '../config'; +import { Ban } from '../types'; -export async function checkExpiredBans(client: Client) { - const guild = await client.guilds.fetch(config.DISCORD_GUILD_ID!); +// --------------- +export async function checkExpiredBans(client: CommandClient) { + const guild = client.guilds.get(config.DISCORD_GUILD_ID!); if (!guild) return; const date = new Date(); for (const ban of await Database.getExpiredBans(date)) { - try { - await guild.bans.remove(ban.user_id); - } catch (e) { - console.error(`Error unbanning user: ${ban.user_id}`); - console.error(e); - } + await guild.unbanMember(ban.user_id, 'Ban Expired.'); } - await Database.pruneExpiredBans(date); + await Database.db.all('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date); } \ No newline at end of file diff --git a/src/utils/embed-utils.ts b/src/utils/embed-utils.ts index 35aaa96..40e24d2 100644 --- a/src/utils/embed-utils.ts +++ b/src/utils/embed-utils.ts @@ -1,18 +1,15 @@ -import { APIEmbed, Client } from 'discord.js'; +import { CommandClient, Constants } from 'athena-prime'; -type EmbedSeverity = - | 'default' - | 'info' - | 'warning' - | 'error' - | 'success' +type Severity = 'info' | 'warning' | 'error' | 'success'; -export function CreateDefaultEmbed(context: Client): APIEmbed { +export function CreateDefaultEmbed(context: CommandClient): Constants.APIEmbed { if (!context.user) return {}; return { + color: 0x014995, + footer: { - icon_url: context.user.avatarURL()!, + icon_url: context.user.avatarURL, text: context.user.username }, @@ -20,9 +17,8 @@ export function CreateDefaultEmbed(context: Client): APIEmbed { }; } -export function SetSeverity(severity: EmbedSeverity): APIEmbed { +export function SetSeverity(severity: Severity): Constants.APIEmbed { switch (severity) { - case 'default': return { color: 0x014995 }; case 'info': return { color: 0x5865F2 }; case 'warning': return { color: 0xFEE75C }; case 'error': return { color: 0xED4245 }; diff --git a/src/utils/private-help-utils.ts b/src/utils/private-help-utils.ts index 22271bb..02ae3b0 100644 --- a/src/utils/private-help-utils.ts +++ b/src/utils/private-help-utils.ts @@ -1,25 +1,24 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, ThreadChannel, UserContextMenuCommandInteraction } from 'discord.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, UserContextMenuCommandInteraction } from 'discord.js'; import { Database } from '../shared/Database'; import { createTextInput, createYesNoMenu } from './modal-utils'; -export async function closeOldTickets(client: Client) { +import { CommandClient, ThreadChannel } from 'athena-prime'; + +// TODO: Handle Modmail Errors. +export async function closeOldTickets(client: CommandClient) { for (const ticket of await Database.getAllOpenPrivateHelpTickets()) { - try { - const thread = await client.channels.fetch(ticket.thread_id) as ThreadChannel; - const latestMessage = (await thread.messages.fetch({ limit: 1 })).at(0); - if (latestMessage && latestMessage.createdTimestamp <= Date.now() - 432e6) { - await Database.closePrivateHelpTicket(thread.id); + const thread = client.getChannel(ticket.thread_id) as ThreadChannel; + const latestMessage = (await (thread.getMessages({ limit: 1 }))).at(0); - await thread.send('This ticket has been closed due to inactivity.'); + if (latestMessage && latestMessage.timestamp <= Date.now() - 432e6) { + await Database.closePrivateHelpTicket(thread.id); - thread.edit({ - archived: true, - locked: true - }); - } - } catch (e) { - console.error('Error closing ticket due to inactivity:'); - console.error(e); + await thread.createMessage('This ticket has been closed due to inactivity.'); + + thread.edit({ + archived: true, + locked: true + }); } } } -- 2.52.0 From efd5df89ca410eb5bb40bc6070188895b7d118e5 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 11:16:21 +0800 Subject: [PATCH 02/14] (rewrite): Updated more events. --- src/events/guildCreate.ts | 2 ++ src/events/handle-thread-create.ts | 11 ------ src/events/handle-voice-state-update.ts | 46 ------------------------- src/events/index.ts | 9 +++-- src/events/threadCreate.ts | 14 ++++++++ src/events/voiceChannelJoin.ts | 38 ++++++++++++++++++++ src/events/voiceChannelLeft.ts | 38 ++++++++++++++++++++ src/events/voiceChannelSwitch.ts | 38 ++++++++++++++++++++ 8 files changed, 136 insertions(+), 60 deletions(-) delete mode 100644 src/events/handle-thread-create.ts delete mode 100644 src/events/handle-voice-state-update.ts create mode 100644 src/events/threadCreate.ts create mode 100644 src/events/voiceChannelJoin.ts create mode 100644 src/events/voiceChannelLeft.ts create mode 100644 src/events/voiceChannelSwitch.ts diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index df1e16c..62f4ddc 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -4,6 +4,8 @@ import { CommandClient, Event, Guild } from 'athena-prime'; // Internal Imports import { Database } from '../shared/Database'; +// --------------- + class GuildCreateEvent extends Event { event: string = 'guildCreate' as const; diff --git a/src/events/handle-thread-create.ts b/src/events/handle-thread-create.ts deleted file mode 100644 index a3729da..0000000 --- a/src/events/handle-thread-create.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AnyThreadChannel } from 'discord.js'; - -export async function handleThreadCreate(thread: AnyThreadChannel, newlyCreated: boolean) { - try { - await thread.join(); - } catch (e) { - console.error('Failed to join thread:'); - console.error(e); - } - -} \ No newline at end of file diff --git a/src/events/handle-voice-state-update.ts b/src/events/handle-voice-state-update.ts deleted file mode 100644 index 61d6f2c..0000000 --- a/src/events/handle-voice-state-update.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Guild, GuildMember, GuildTextBasedChannel, time, VoiceBasedChannel, VoiceState } from 'discord.js'; -import { Database } from '../shared/Database'; - -export async function handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState) { - // The logChannel declaration being inside is purposeful, as this event is fired a lot for users talking. - if (newState.channelId != null && oldState.channelId != null && newState.channelId != oldState.channelId) { - const logChannel = await getVoiceLogsChannel(newState.guild); - if (!logChannel) return; - - await sendMovedMessage(logChannel, newState.member!, oldState.channel!, newState.channel!); - } else if (oldState.channelId == null && newState.channelId != null) { - const logChannel = await getVoiceLogsChannel(newState.guild); - if (!logChannel) return; - - await sendJoinMessage(logChannel, newState.member!, newState.channel!); - } else if (newState.channelId == null && oldState.channelId != null) { - const logChannel = await getVoiceLogsChannel(newState.guild); - if (!logChannel) return; - - await sendLeftMessage(logChannel, newState.member!, oldState.channel!); - } -} - -async function sendJoinMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) { - await channel.send(`${member} joined ${voiceChannel} at ${time()}`); -} - -async function sendLeftMessage(channel: GuildTextBasedChannel, member: GuildMember, voiceChannel: VoiceBasedChannel) { - await channel.send(`${member} left ${voiceChannel} at ${time()}`); -} - -async function sendMovedMessage(channel: GuildTextBasedChannel, member: GuildMember, oldVoiceChannel: VoiceBasedChannel, newVoiceChannel: VoiceBasedChannel) { - await channel.send(`${member} moved from ${oldVoiceChannel} to ${newVoiceChannel} at ${time()}`); -} - -async function getVoiceLogsChannel(guild: Guild): Promise { - const settings = await Database.getGuildSettings(guild.id); - - if (!settings || !settings.voice_logs_channel_id) return; - - const channel = await guild.channels.fetch(settings.voice_logs_channel_id); - - if (!channel || !channel.isSendable()) return; - - return channel as GuildTextBasedChannel; -} \ No newline at end of file diff --git a/src/events/index.ts b/src/events/index.ts index 72d583f..62133c6 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -3,7 +3,10 @@ import guildBanRemove from './guildBanRemove'; import guildCreate from './guildCreate'; import guildMemberAdd from './guildMemberAdd'; export * from './handle-message'; -export * from './handle-thread-create'; -export * from './handle-voice-state-update'; +import threadCreate from './threadCreate'; -export default [guildBanRemove, guildCreate, guildMemberAdd]; \ No newline at end of file +import voiceChannelJoin from './voiceChannelJoin'; +import voiceChannelLeft from './voiceChannelLeft'; +import voiceChannelSwitch from './voiceChannelSwitch'; + +export default [guildBanRemove, guildCreate, guildMemberAdd, threadCreate, voiceChannelJoin, voiceChannelLeft, voiceChannelSwitch]; \ No newline at end of file diff --git a/src/events/threadCreate.ts b/src/events/threadCreate.ts new file mode 100644 index 0000000..9c89488 --- /dev/null +++ b/src/events/threadCreate.ts @@ -0,0 +1,14 @@ +// External Imports +import { CommandClient, Event, ThreadChannel } from 'athena-prime'; + +// --------------- + +class ThreadCreateEvent extends Event { + event: string = 'threadCreate' as const; + + async handle(context: CommandClient, channel: ThreadChannel) { + await context.joinThread(channel.id); + } +} + +export default new ThreadCreateEvent('threadCreate'); \ No newline at end of file diff --git a/src/events/voiceChannelJoin.ts b/src/events/voiceChannelJoin.ts new file mode 100644 index 0000000..1df33d4 --- /dev/null +++ b/src/events/voiceChannelJoin.ts @@ -0,0 +1,38 @@ +// External Imports +import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime"; + +// Internal Imports +import { Database } from "../shared/Database"; +import { SetSeverity } from "../utils"; + +// --------------- + +class VoiceChannelJoinEvent extends Event { + event: string = 'voiceChannelJoin' as const; + + async handle(context: CommandClient, member: Member, channel: GuildVoiceChannel) { + const settings = await Database.GetOrCreateSettings(channel.guild.id); + if (!settings.voice_logs_channel_id) return; + + await context.createMessage(settings.voice_logs_channel_id, { + embed: { + ...SetSeverity('success'), + + author: { + name: member.username, + icon_url: member.avatarURL + }, + + fields: [ + { name: 'Member', value: `${member.username} (${member.mention})`, inline: true }, + { name: 'Channel', value: `${channel.mention}`, inline: true }, + { name: 'Time', value: ``, inline: true }, + ], + + title: 'Joined Voice Channel' + } + }); + } +} + +export default new VoiceChannelJoinEvent('voiceChannelJoin'); \ No newline at end of file diff --git a/src/events/voiceChannelLeft.ts b/src/events/voiceChannelLeft.ts new file mode 100644 index 0000000..2aa13a2 --- /dev/null +++ b/src/events/voiceChannelLeft.ts @@ -0,0 +1,38 @@ +// External Imports +import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime"; + +// Internal Imports +import { Database } from "../shared/Database"; +import { SetSeverity } from "../utils"; + +// --------------- + +class VoiceChannelLeftEvent extends Event { + event: string = 'voiceChannelLeft' as const; + + async handle(context: CommandClient, member: Member, channel: GuildVoiceChannel) { + const settings = await Database.GetOrCreateSettings(channel.guild.id); + if (!settings.voice_logs_channel_id) return; + + await context.createMessage(settings.voice_logs_channel_id, { + embed: { + author: { + name: member.username, + icon_url: member.avatarURL + }, + + ...SetSeverity('error'), + + fields: [ + { name: 'Member', value: `${member.username} (${member.mention})`, inline: true }, + { name: 'Channel', value: `${channel.mention}`, inline: true }, + { name: 'Time', value: ``, inline: true }, + ], + + title: 'Left Voice Channel' + } + }); + } +} + +export default new VoiceChannelLeftEvent('voiceChannelLeft'); \ No newline at end of file diff --git a/src/events/voiceChannelSwitch.ts b/src/events/voiceChannelSwitch.ts new file mode 100644 index 0000000..1e3168b --- /dev/null +++ b/src/events/voiceChannelSwitch.ts @@ -0,0 +1,38 @@ +// External Imports +import { CommandClient, Event, Member, GuildVoiceChannel } from "athena-prime"; + +// Internal Imports +import { Database } from "../shared/Database"; +import { SetSeverity } from "../utils"; + +// --------------- + +class VoiceChannelSwitchEvent extends Event { + event: string = 'voiceChannelSwitch' as const; + + async handle(context: CommandClient, member: Member, channel: GuildVoiceChannel, old: GuildVoiceChannel) { + const settings = await Database.GetOrCreateSettings(channel.guild.id); + if (!settings.voice_logs_channel_id) return; + + await context.createMessage(settings.voice_logs_channel_id, { + embed: { + ...SetSeverity('warning'), + + author: { + name: member.username, + icon_url: member.avatarURL + }, + + fields: [ + { name: 'Member', value: `${member.username} (${member.mention})`, inline: true }, + { name: 'Channel', value: `${old.mention} -> ${channel.mention}`, inline: true }, + { name: 'Time', value: ``, inline: true }, + ], + + title: 'Moved Voice Channel' + } + }); + } +} + +export default new VoiceChannelSwitchEvent('voiceChannelSwitch'); \ No newline at end of file -- 2.52.0 From d2b30ad0609b27d8c668f0641158f2fd614d8221 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 17:33:35 +0800 Subject: [PATCH 03/14] (rewrite): Began organising commands and polished `/name-sync` --- src/commands/{ => infractions}/ban.ts | 99 +++++++++++---------------- src/commands/infractions/hardban.ts | 33 +++++++++ src/commands/name-sync.ts | 36 ---------- src/commands/utilities/name-sync.ts | 62 +++++++++++++++++ src/events/handle-audit-log-create.ts | 94 ------------------------- src/utils/name-sync.ts | 28 -------- 6 files changed, 134 insertions(+), 218 deletions(-) rename src/commands/{ => infractions}/ban.ts (58%) create mode 100644 src/commands/infractions/hardban.ts delete mode 100644 src/commands/name-sync.ts create mode 100644 src/commands/utilities/name-sync.ts delete mode 100644 src/events/handle-audit-log-create.ts delete mode 100644 src/utils/name-sync.ts diff --git a/src/commands/ban.ts b/src/commands/infractions/ban.ts similarity index 58% rename from src/commands/ban.ts rename to src/commands/infractions/ban.ts index 2136850..2e53645 100644 --- a/src/commands/ban.ts +++ b/src/commands/infractions/ban.ts @@ -1,67 +1,11 @@ -import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js'; -import { Database } from '../shared/Database'; -import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../utils'; +import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js'; +import { Database } from '../../shared/Database'; +import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../../utils'; export default { - name: 'ban', - data: new SlashCommandBuilder() - .setName('ban') - .setDescription('Bans a user.') - .setIntegrationTypes(ApplicationIntegrationType.GuildInstall) - .setContexts(InteractionContextType.Guild) - .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) - .addStringOption(option => - option - .setName('user') - .setDescription('The discord user mention, or ID, to ban.') - .setRequired(true) - ) - .addStringOption(option => - option - .setName('reason') - .setDescription('The reason for the ban') - .setRequired(false) - .setMaxLength(400) - ) - .addNumberOption(option => - option - .setName('hours') - .setDescription('The duration of the ban, added with other options (0 for permanent).') - .setRequired(false) - ) - .addNumberOption(option => - option - .setName('minutes') - .setDescription('The duration of the ban, added with other options (0 for permanent).') - .setRequired(false) - ) - .addNumberOption(option => - option - .setName('seconds') - .setDescription('The duration of the ban, added with other options (0 for permanent).') - .setRequired(false) - ) - .addNumberOption(option => - option - .setName('delete-message-days') - .setDescription('How far back to delete messages (in days, default: 0 days).') - .setRequired(false) - .setMinValue(0) - .setMaxValue(7) - ) - .addBooleanOption(option => - option - .setName('full-ban') - .setDescription('Whether or not to prevent the user from joining on known alts (and ban all existing alts).') - .setRequired(false) - ), handler: async function (client: Client, interaction: ChatInputCommandInteraction) { await deferInteraction(interaction); - if (!interaction.guild) return interaction.editReply('This command must be used in a server'); - - if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.'); - const input = interaction.options.getString('user', true); const idToUse = getIdFromInput(input); @@ -133,4 +77,39 @@ async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); } -} \ No newline at end of file +} + +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// --------------- + +@SlashCommand( + new CommandBuilder('ban', 'Ban a user.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.BanMembers) + .addUserOption('user', 'The user to ban.', true) + .addStringOption({ name: 'duration', description: 'The ban duration.', required: false }) + .addStringOption({ name: 'reason', description: 'The ban reason.', required: false }) + .addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false }) +) +class BanCommand extends Command { + cooldown = 5; + userPermissions = [Constants.PermissionFlagsBits.BanMembers]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, fetchedData?: any) { + if (!interaction.inGuild() || !interaction.member.highestRole) return; + await interaction.defer(); + + const user = interaction.getRequiredMember('user'); + const duration = interaction.getString('duration'); + const reason = interaction.getString('reason') || 'No reason specified.'; + const messageDays = interaction.getNumber('cleanup') || 0; + + await context.banGuildMember(interaction.guild.id, user.id, messageDays, reason); + } +} + +export default new BanCommand('ban'); \ No newline at end of file diff --git a/src/commands/infractions/hardban.ts b/src/commands/infractions/hardban.ts new file mode 100644 index 0000000..35d67fc --- /dev/null +++ b/src/commands/infractions/hardban.ts @@ -0,0 +1,33 @@ +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// --------------- + +@SlashCommand( + new CommandBuilder('hard-ban', 'Permanently bans a user, and all known alternate accounts.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.BanMembers) + .addUserOption('user', 'The user to ban.', true) + .addStringOption({ name: 'reason', description: 'The ban reason.', required: false }) + .addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false }) +) +class HardBanCommand extends Command { + cooldown = 5; + userPermissions = [Constants.PermissionFlagsBits.BanMembers]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, fetchedData?: any) { + if (!interaction.inGuild() || !interaction.member.highestRole) return; + await interaction.defer(); + + const user = interaction.getRequiredMember('user'); + const duration = interaction.getString('duration'); + const reason = interaction.getString('reason') || 'No reason specified.'; + const messageDays = interaction.getNumber('cleanup') || 0; + + // Nix -> Tarrgon: Please implement the hard-ban here :3 + } +} + +export default new HardBanCommand('hardban'); \ No newline at end of file diff --git a/src/commands/name-sync.ts b/src/commands/name-sync.ts deleted file mode 100644 index 63ffeea..0000000 --- a/src/commands/name-sync.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, SlashCommandBuilder } from 'discord.js'; -import { config } from '../config'; -import { syncName } from '../utils'; - -export default { - name: 'name-sync', - data: new SlashCommandBuilder() - .setName('name-sync') - .setDescription('Sync your discord nickname to your e621 name.') - .setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall) - .setContexts(InteractionContextType.Guild, InteractionContextType.BotDM) - .addIntegerOption(option => - option - .setName('id') - .setDescription('The id of the e621 user to sync your nickname to.') - .setRequired(false) - ), - handler: async function (client: Client, interaction: ChatInputCommandInteraction) { - await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); - const id = interaction.options.getInteger('id'); - - const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!); - - if (!guild) { - return interaction.editReply('An error has occurred. Please try again later.'); - } - - const member = await guild.members.fetch(interaction.user.id); - - if (!member || !guild.members.me) { - return interaction.editReply('An error has occurred. Please try again later.'); - } - - await syncName(interaction, member, id); - } -}; \ No newline at end of file diff --git a/src/commands/utilities/name-sync.ts b/src/commands/utilities/name-sync.ts new file mode 100644 index 0000000..042ddf9 --- /dev/null +++ b/src/commands/utilities/name-sync.ts @@ -0,0 +1,62 @@ +import { Database } from '../../shared/Database'; +import { E621User } from '../../types'; +import { CreateDefaultEmbed, getE621User, SetSeverity } from '../../utils'; + +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, Member, SlashCommand } from 'athena-prime'; + +// --------------- + +async function fetchUser(member: Member, id: number | null): Promise { + const syncedIds = await Database.getE621Ids(member.id); + + if (!id || !syncedIds.includes(id)) + return await getE621User(syncedIds[0]); + else + return await getE621User(id); +} + +@SlashCommand( + new CommandBuilder('name-sync', 'Sync your Discord username with your e621 username.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .addNumberOption({ name: 'id', description: 'The ID of the user to sync your name to.', required: false, max_value: 16 }) +) +class NameSyncCommand extends Command { + cooldown = 60; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild()) return; + await interaction.defer(); + + const id = interaction.getNumber('id'); + const user = await fetchUser(interaction.member, id); + + if (!user) { + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('error'), + + description: 'I was unable to get a user entry for your account or provided id. Please try again, or contact an administrator.' + }] + }); + + return; + } + + await interaction.member.edit({ nick: user.name }, 'Member requested a name-sync.'); + + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('success'), + + description: `Successfully updated your display name. Hello \`${user.name}\`!` + }] + }); + } +} + +export default new NameSyncCommand('name-sync'); \ No newline at end of file diff --git a/src/events/handle-audit-log-create.ts b/src/events/handle-audit-log-create.ts deleted file mode 100644 index f875e7e..0000000 --- a/src/events/handle-audit-log-create.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { APIEmbedField, APIRole, AuditLogEvent, EmbedBuilder, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js'; -import { Database } from '../shared/Database'; -import { formatChanges, formatExtras, formatSnowflake, getTargetType } from '../utils'; - -const IGNORED_ACTIONS = [ - AuditLogEvent.MemberMove, - // Handled by automod. - AuditLogEvent.AutoModerationFlagToChannel -]; - -export async function handleAuditLogCreate(entry: GuildAuditLogsEntry, guild: Guild) { - if (!await shouldLog(entry, guild)) return; - const settings = await Database.getGuildSettings(guild.id); - - if (!settings || !settings.audit_logs_channel_id) return; - - const channel = await guild.channels.fetch(settings.audit_logs_channel_id); - - if (!channel || !channel.isSendable()) return; - - const fields: APIEmbedField[] = [ - { - name: 'Actor', - value: `<@${entry.executorId}>`, - inline: true - } - ]; - - if (entry.targetId) { - const targetType = getTargetType(entry.action); - fields.push({ - name: 'Target', - value: formatSnowflake(entry.targetId, targetType), - inline: true - }); - } - - if (entry.reason) { - fields.push({ - name: 'Reason', - value: entry.reason, - inline: true - }); - } - - if (entry.changes && entry.changes.length > 0) { - fields.push({ - name: 'Changes', - value: formatChanges(entry), - inline: false - }); - } - - if (entry.extra) { - fields.push({ - name: 'Options', - value: formatExtras(entry, guild), - inline: false - }); - } - - const embed = new EmbedBuilder() - .setTitle(Object.keys(AuditLogEvent)[Object.values(AuditLogEvent).indexOf(entry.action)]) - .setTimestamp(Number(SnowflakeUtil.decode(entry.id).timestamp)) - .addFields(...fields); - - channel.send({ embeds: [embed] }); -} - -async function shouldLog(entry: GuildAuditLogsEntry, guild: Guild): Promise { - if (!entry.executorId) return true; - - if (IGNORED_ACTIONS.some(a => entry.action == a)) return false; - - if (entry.action == AuditLogEvent.MemberRoleUpdate) { - return await shouldLogRoleChanges(entry as GuildAuditLogsEntry, guild); - } - - return true; -} - -async function shouldLogRoleChanges(entry: GuildAuditLogsEntry, guild: Guild): Promise { - for (const change of entry.changes) { - // Get role changes from the log. - const roles = (await Promise.all((change.new! as Pick[]).map(c => guild.roles.fetch(c.id)))); - - // Check if role is part of onboarding. - for (const role of roles) { - if (role && !role.flags.has(RoleFlags.InPrompt)) return true; - } - } - - return false; -} \ No newline at end of file diff --git a/src/utils/name-sync.ts b/src/utils/name-sync.ts deleted file mode 100644 index 842957d..0000000 --- a/src/utils/name-sync.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ChatInputCommandInteraction, GuildMember, ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { E621User } from '../types'; -import { getE621User } from './e621-utils'; - -export async function syncName(interaction: ChatInputCommandInteraction | UserContextMenuCommandInteraction | ModalSubmitInteraction, member: GuildMember, id: number | null) { - if (member.roles.highest.comparePositionTo(member.guild.members.me!.roles.highest) > 0) { - const res = interaction.user.id == member.id ? 'your' : 'their'; - return interaction.editReply(`I am unable to set ${res} nickname as ${res} role is higher than mine.`); - } - - const availableIds = await Database.getE621Ids(interaction.user.id); - - let e621User: E621User | null; - - if (!id || !availableIds.includes(id)) { - e621User = await getE621User(availableIds[0]); - } else { - e621User = await getE621User(id); - } - - if (!e621User) { - return interaction.editReply("Couldn't figure out what your name was. Please contact an administrator."); - } - - await member.setNickname(e621User.name); - interaction.editReply(`Nickname set to: ${e621User.name}`); -} \ No newline at end of file -- 2.52.0 From fb2412963434644752abf1c7f45b780edc84541f Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 17:54:11 +0800 Subject: [PATCH 04/14] (rewrite): Rewritten `/rename` to `/rename-general`. --- src/commands/admin/rename-channel.ts | 68 ++++++++++++++++++++++++++++ src/commands/rename.ts | 51 --------------------- src/commands/utilities/name-sync.ts | 7 +-- 3 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 src/commands/admin/rename-channel.ts delete mode 100644 src/commands/rename.ts diff --git a/src/commands/admin/rename-channel.ts b/src/commands/admin/rename-channel.ts new file mode 100644 index 0000000..0b5c439 --- /dev/null +++ b/src/commands/admin/rename-channel.ts @@ -0,0 +1,68 @@ +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// Internal Imports +import { Database } from '../../shared/Database'; +import { CreateDefaultEmbed, SetSeverity } from '../../utils'; + +// --------------- + +@SlashCommand( + new CommandBuilder('rename-channel', 'Rename the general channel.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.ManageChannels) + .addStringOption({ name: 'name', description: 'The new general chat name.', required: true }) +) +class RenameGeneralCommand extends Command { + cooldown = 360; + userPermissions = [Constants.PermissionFlagsBits.ManageChannels]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild()) return; + interaction.defer(); + + const settings = await Database.GetOrCreateSettings(interaction.guild.id); + if (!settings.general_chat_id) { + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('warning'), + + description: 'No general channel has been configured. Please check your configuration and try again.' + }] + }); + + return; + } + + const channel = context.getChannel(settings.general_chat_id); + if (!channel || !channel.isTextBased()) { + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('error'), + + description: 'There was an issue trying to locate the configured general channel role. Please check your configuration and try again.' + }] + }); + + return; + } + + const name = interaction.getRequiredString('name'); + await context.editChannel(settings.general_chat_id, { name: name }); + + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('success'), + + description: `Successfully updated ${channel.mention}` + }] + }); + } +} + +export default new RenameGeneralCommand('name-sync'); \ No newline at end of file diff --git a/src/commands/rename.ts b/src/commands/rename.ts deleted file mode 100644 index bfb7776..0000000 --- a/src/commands/rename.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, PermissionFlagsBits, RateLimitError, SlashCommandBuilder } from 'discord.js'; -import { msToHuman } from '../utils'; -import { Database } from '../shared/Database'; - -export default { - name: 'rename', - data: new SlashCommandBuilder() - .setName('rename') - .setDescription('Rename the general channel.') - .setIntegrationTypes(ApplicationIntegrationType.GuildInstall) - .setContexts(InteractionContextType.Guild) - .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) - .addStringOption(option => - option - .setName('new-name') - .setDescription('The new name of the general channel.') - .setRequired(true) - ), - handler: async function (client: Client, interaction: ChatInputCommandInteraction) { - const guildSettings = await Database.getGuildSettings(interaction.guildId!); - - if (!guildSettings || !guildSettings.general_chat_id) { - return interaction.reply('No general chat id found.'); - } - - const name = interaction.options.getString('new-name', true); - - if (name.length > 100) { - return interaction.reply('Name must be less than 100 characters in length.'); - } - - const channel = await interaction.guild!.channels.fetch(guildSettings.general_chat_id)!; - - if (!channel) { - return interaction.reply('No general chat id found.'); - } - - try { - await channel.setName(name); - - interaction.reply(`Renamed general to ${channel.name}`); - } catch (e: any) { - if (e instanceof RateLimitError) { - return interaction.reply(`Name change limited. Try again in ${msToHuman(e.retryAfter)}`); - } - - console.error(e); - return interaction.reply('An error has occurred.'); - } - } -}; \ No newline at end of file diff --git a/src/commands/utilities/name-sync.ts b/src/commands/utilities/name-sync.ts index 042ddf9..e7531a9 100644 --- a/src/commands/utilities/name-sync.ts +++ b/src/commands/utilities/name-sync.ts @@ -1,10 +1,11 @@ +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, Member, SlashCommand } from 'athena-prime'; + +// Internal Imports import { Database } from '../../shared/Database'; import { E621User } from '../../types'; import { CreateDefaultEmbed, getE621User, SetSeverity } from '../../utils'; -// External Imports -import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, Member, SlashCommand } from 'athena-prime'; - // --------------- async function fetchUser(member: Member, id: number | null): Promise { -- 2.52.0 From a4d97126eea068f5afd6fd0417f18e6c114f734a Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 17:56:51 +0800 Subject: [PATCH 05/14] (rewrite): Add maximum length limitation. --- src/commands/admin/rename-channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/admin/rename-channel.ts b/src/commands/admin/rename-channel.ts index 0b5c439..ef92e35 100644 --- a/src/commands/admin/rename-channel.ts +++ b/src/commands/admin/rename-channel.ts @@ -13,7 +13,7 @@ import { CreateDefaultEmbed, SetSeverity } from '../../utils'; .setContexts(Constants.InteractionContextType.Guild) .setCommandType(Constants.ApplicationCommandType.ChatInput) .setMemberPermission(Constants.PermissionFlagsBits.ManageChannels) - .addStringOption({ name: 'name', description: 'The new general chat name.', required: true }) + .addStringOption({ name: 'name', description: 'The new general chat name.', required: true, max_length: 32 }) ) class RenameGeneralCommand extends Command { cooldown = 360; -- 2.52.0 From 2cbb16358d8be4f92c0d87f1aea24aecfc2c5621 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 18:20:36 +0800 Subject: [PATCH 06/14] (rewrite): Updated `/rename-channel` and rewritten `/database`. --- src/commands/admin/database.ts | 49 ++++++++++++++++++++++++++++ src/commands/admin/rename-channel.ts | 8 ++--- src/commands/database.ts | 48 --------------------------- 3 files changed, 53 insertions(+), 52 deletions(-) create mode 100644 src/commands/admin/database.ts delete mode 100644 src/commands/database.ts diff --git a/src/commands/admin/database.ts b/src/commands/admin/database.ts new file mode 100644 index 0000000..9c8f9de --- /dev/null +++ b/src/commands/admin/database.ts @@ -0,0 +1,49 @@ +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// Internal Imports +import { Database } from '../../shared/Database'; +import { CreateDefaultEmbed, SetSeverity } from '../../utils'; + +// --------------- + +@SlashCommand( + new CommandBuilder('database', "Essential database functions. THESE ARE POTENTIALLY DESTRUCTIVE ACTIONS, BE SURE!") + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.Administrator) + + .addSubcommand('purge-user', 'Purge user messages from the database.', builder => + builder.addUserOption('user', 'The user to purge.', true) + ) +) +class DatabaseCommand extends Command { + cooldown = 60; + userPermissions = [Constants.PermissionFlagsBits.Administrator]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild() || !interaction.subcommand) return; + + await interaction.defer(); + + switch (interaction.subcommand.at(0)) { + case 'purge-user': + const user = interaction.getRequiredMember('user'); + await Database.purgeMessagesFrom(user.id); + + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('success'), + + description: `Executed user-purge on ${user.mention}.` + }] + }); + + return; + } + } +} + +export default new DatabaseCommand('database'); \ No newline at end of file diff --git a/src/commands/admin/rename-channel.ts b/src/commands/admin/rename-channel.ts index ef92e35..c397421 100644 --- a/src/commands/admin/rename-channel.ts +++ b/src/commands/admin/rename-channel.ts @@ -8,12 +8,12 @@ import { CreateDefaultEmbed, SetSeverity } from '../../utils'; // --------------- @SlashCommand( - new CommandBuilder('rename-channel', 'Rename the general channel.') + new CommandBuilder('rename-general', 'Rename the general channel.') .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) .setContexts(Constants.InteractionContextType.Guild) .setCommandType(Constants.ApplicationCommandType.ChatInput) .setMemberPermission(Constants.PermissionFlagsBits.ManageChannels) - .addStringOption({ name: 'name', description: 'The new general chat name.', required: true, max_length: 32 }) + .addStringOption({ name: 'name', description: 'The new general chat name.', required: true, max_length: 64 }) ) class RenameGeneralCommand extends Command { cooldown = 360; @@ -21,7 +21,7 @@ class RenameGeneralCommand extends Command { async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { if (!interaction.inGuild()) return; - interaction.defer(); + await interaction.defer(); const settings = await Database.GetOrCreateSettings(interaction.guild.id); if (!settings.general_chat_id) { @@ -65,4 +65,4 @@ class RenameGeneralCommand extends Command { } } -export default new RenameGeneralCommand('name-sync'); \ No newline at end of file +export default new RenameGeneralCommand('rename-general'); \ No newline at end of file diff --git a/src/commands/database.ts b/src/commands/database.ts deleted file mode 100644 index ebf8e76..0000000 --- a/src/commands/database.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; -import { Database } from '../shared/Database'; -import { getIdFromInput } from '../utils'; - -export default { - name: 'database', - data: new SlashCommandBuilder() - .setName('database') - .setDescription('Database access.') - .setIntegrationTypes(ApplicationIntegrationType.GuildInstall) - .setContexts(InteractionContextType.Guild) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) - .addSubcommandGroup(subcommandGroup => - subcommandGroup - .setName('messages') - .setDescription('Access to messages in the database.') - .addSubcommand(subcommand => - subcommand - .setName('purge') - .setDescription("Purge a user's messages from the database.") - .addStringOption(option => - option - .setName('user') - .setDescription('The discord user id, or mention, of the user.') - .setRequired(true) - ) - ) - ), - handler: async function (client: Client, interaction: ChatInputCommandInteraction) { - await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); - - const group = interaction.options.getSubcommandGroup(true); - - const subcommand = interaction.options.getSubcommand(true); - - if (group == 'messages') { - if (subcommand == 'purge') { - const discordUserInput = interaction.options.getString('discord-user', true); - - const idToUse = getIdFromInput(discordUserInput); - - await Database.purgeMessagesFrom(idToUse); - - return interaction.editReply('Messages purged.'); - } - } - } -}; \ No newline at end of file -- 2.52.0 From 828abaf796056e36bbc2e159b8c53524f79e7a77 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:01:16 +0800 Subject: [PATCH 07/14] (rewrite): Mostly implemented `/ban`. `/ban` & `/hardban` will need to be worked on properly by Tarrgon who knows more about the hell that is the database code. --- src/commands/infractions/ban.ts | 115 --------------- src/commands/moderation/ban.ts | 137 ++++++++++++++++++ .../{infractions => moderation}/hardban.ts | 11 +- 3 files changed, 143 insertions(+), 120 deletions(-) delete mode 100644 src/commands/infractions/ban.ts create mode 100644 src/commands/moderation/ban.ts rename src/commands/{infractions => moderation}/hardban.ts (70%) diff --git a/src/commands/infractions/ban.ts b/src/commands/infractions/ban.ts deleted file mode 100644 index 2e53645..0000000 --- a/src/commands/infractions/ban.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js'; -import { Database } from '../../shared/Database'; -import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../../utils'; - -export default { - handler: async function (client: Client, interaction: ChatInputCommandInteraction) { - await deferInteraction(interaction); - - const input = interaction.options.getString('user', true); - - const idToUse = getIdFromInput(input); - const reason = interaction.options.getString('reason') ?? ''; - - const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6; - const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000; - const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000; - - const duration = hours + minutes + seconds; - - const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400; - - const fullBan = interaction.options.getBoolean('full-ban') ?? false; - - let banMember: GuildMember | null = null; - - try { - banMember = await interaction.guild.members.fetch(idToUse); - } catch (e) { - // Member not in server. - } - - const member = await interaction.guild.members.fetch(interaction.user.id); - - if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) { - return await interaction.editReply('You do not have permission to ban this user.'); - } - - if (banMember && !banMember.bannable) { - return await interaction.editReply('I do not have permission to ban this user.'); - } - - const expiresAt = new Date(Date.now() + duration); - - await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan); - - try { - await interaction.guild.bans.create(idToUse, { - reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(), - deleteMessageSeconds: deleteMessageDays - }); - } catch (e) { - console.error(e); - return await interaction.editReply("Error banning user (couldn't ban)."); - } - - if (fullBan) { - const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild); - - await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt); - } - - await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`); - } -}; - -async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) { - for (const data of altData) { - if (data.type == 'discord') { - try { - if (!data.banned) { - await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim()); - } - } catch (e) { - console.error(e); - } - } - - await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); - } -} - -// External Imports -import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; - -// --------------- - -@SlashCommand( - new CommandBuilder('ban', 'Ban a user.') - .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) - .setContexts(Constants.InteractionContextType.Guild) - .setCommandType(Constants.ApplicationCommandType.ChatInput) - .setMemberPermission(Constants.PermissionFlagsBits.BanMembers) - .addUserOption('user', 'The user to ban.', true) - .addStringOption({ name: 'duration', description: 'The ban duration.', required: false }) - .addStringOption({ name: 'reason', description: 'The ban reason.', required: false }) - .addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false }) -) -class BanCommand extends Command { - cooldown = 5; - userPermissions = [Constants.PermissionFlagsBits.BanMembers]; - - async handleCommand(context: CommandClient, interaction: CommandInteraction, fetchedData?: any) { - if (!interaction.inGuild() || !interaction.member.highestRole) return; - await interaction.defer(); - - const user = interaction.getRequiredMember('user'); - const duration = interaction.getString('duration'); - const reason = interaction.getString('reason') || 'No reason specified.'; - const messageDays = interaction.getNumber('cleanup') || 0; - - await context.banGuildMember(interaction.guild.id, user.id, messageDays, reason); - } -} - -export default new BanCommand('ban'); \ No newline at end of file diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts new file mode 100644 index 0000000..677d513 --- /dev/null +++ b/src/commands/moderation/ban.ts @@ -0,0 +1,137 @@ +import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js'; +import { Database } from '../../shared/Database'; +import { AltData, comprehensiveAltLookupFromDiscord, CreateDefaultEmbed, deferInteraction, getIdFromInput, SetSeverity } from '../../utils'; + +// export default { +// handler: async function (client: Client, interaction: ChatInputCommandInteraction) { +// await deferInteraction(interaction); + +// const input = interaction.options.getString('user', true); + +// const idToUse = getIdFromInput(input); +// const reason = interaction.options.getString('reason') ?? ''; + +// const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6; +// const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000; +// const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000; + +// const duration = hours + minutes + seconds; + +// const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400; + +// const fullBan = interaction.options.getBoolean('full-ban') ?? false; + +// let banMember: GuildMember | null = null; + +// try { +// banMember = await interaction.guild.members.fetch(idToUse); +// } catch (e) { +// // Member not in server. +// } + +// const member = await interaction.guild.members.fetch(interaction.user.id); + +// if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) { +// return await interaction.editReply('You do not have permission to ban this user.'); +// } + +// if (banMember && !banMember.bannable) { +// return await interaction.editReply('I do not have permission to ban this user.'); +// } + +// const expiresAt = new Date(Date.now() + duration); + +// await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan); + +// try { +// await interaction.guild.bans.create(idToUse, { +// reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(), +// deleteMessageSeconds: deleteMessageDays +// }); +// } catch (e) { +// console.error(e); +// return await interaction.editReply("Error banning user (couldn't ban)."); +// } + +// if (fullBan) { +// const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild); + +// await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt); +// } + +// await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`); +// } +// }; + +// async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) { +// for (const data of altData) { +// if (data.type == 'discord') { +// try { +// if (!data.banned) { +// await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim()); +// } +// } catch (e) { +// console.error(e); +// } +// } + +// await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); +// } +// } + +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// --------------- + +@SlashCommand( + new CommandBuilder('ban', 'Ban a user, optionally cleanup 1 day worth of messages.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .setMemberPermission(Constants.PermissionFlagsBits.BanMembers) + .addUserOption('user', 'The user to ban.', true) + .addStringOption({ name: 'duration', description: '(optional): The duration of the ban. Formatted as 7d12h30m30s.', required: false }) + .addStringOption({ name: 'reason', description: '(optional): The reason for banning the user.', required: false }) + .addBooleanOption('clean', "(optional): Clean the user's messages upwards of a day.", false) + .addBooleanOption('purge', "(optional): Purge the user's messages from the database.", false) +) +class BanCommand extends Command { + cooldown = 5; + userPermissions = [Constants.PermissionFlagsBits.BanMembers]; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild()) return; + await interaction.defer(); + + const user = interaction.getRequiredMember('user'); + const duration = interaction.getString('duration'); + const reason = interaction.getString('reason') || 'No reason specified.'; + const clean = interaction.getBoolean('clean') || false; + const purge = interaction.getBoolean('purge') || false; + + // Nix -> Tarrgon: Could you pretty please hook up the database for this 🥺 + + await context.banGuildMember(interaction.guild.id, user.id, clean ? 1 : undefined, reason); + if (purge) await Database.purgeMessagesFrom(user.id); + + interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('success'), + + description: 'Executed ban action.', + + fields: [ + { name: 'User', value: `${user.username} (${user.mention})`, inline: false }, + { name: 'Duration', value: `${duration || 'Permanent'}`, inline: false }, + { name: 'Reason', value: `${user.username} (${user.mention})`, inline: false }, + { name: 'Clean', value: `${clean ? 'Yes' : 'No'}`, inline: true }, + { name: 'Purge', value: `${purge ? 'Yes' : 'No'}`, inline: true }, + ] + }] + }); + } +} + +export default new BanCommand('ban'); \ No newline at end of file diff --git a/src/commands/infractions/hardban.ts b/src/commands/moderation/hardban.ts similarity index 70% rename from src/commands/infractions/hardban.ts rename to src/commands/moderation/hardban.ts index 35d67fc..09ebf4b 100644 --- a/src/commands/infractions/hardban.ts +++ b/src/commands/moderation/hardban.ts @@ -10,21 +10,22 @@ import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, .setCommandType(Constants.ApplicationCommandType.ChatInput) .setMemberPermission(Constants.PermissionFlagsBits.BanMembers) .addUserOption('user', 'The user to ban.', true) - .addStringOption({ name: 'reason', description: 'The ban reason.', required: false }) - .addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false }) + .addStringOption({ name: 'reason', description: '(optional): The reason for banning the user.', required: false }) + .addBooleanOption('clean', "(optional): Clean the user's messages upwards of a day.", false) + .addBooleanOption('purge', "(optional): Purge the user's messages from the database.", false) ) class HardBanCommand extends Command { cooldown = 5; userPermissions = [Constants.PermissionFlagsBits.BanMembers]; async handleCommand(context: CommandClient, interaction: CommandInteraction, fetchedData?: any) { - if (!interaction.inGuild() || !interaction.member.highestRole) return; + if (!interaction.inGuild()) return; await interaction.defer(); const user = interaction.getRequiredMember('user'); - const duration = interaction.getString('duration'); const reason = interaction.getString('reason') || 'No reason specified.'; - const messageDays = interaction.getNumber('cleanup') || 0; + const clean = interaction.getBoolean('clean') || false; + const purge = interaction.getBoolean('purge') || false; // Nix -> Tarrgon: Please implement the hard-ban here :3 } -- 2.52.0 From 39520149600e230d670d5990099140b841a436ff Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:02:28 +0800 Subject: [PATCH 08/14] (rewrite): Organise :3 --- src/commands/moderation/ban.ts | 85 ++-------------------------------- 1 file changed, 4 insertions(+), 81 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 677d513..0fa6919 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,87 +1,10 @@ -import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js'; -import { Database } from '../../shared/Database'; -import { AltData, comprehensiveAltLookupFromDiscord, CreateDefaultEmbed, deferInteraction, getIdFromInput, SetSeverity } from '../../utils'; - -// export default { -// handler: async function (client: Client, interaction: ChatInputCommandInteraction) { -// await deferInteraction(interaction); - -// const input = interaction.options.getString('user', true); - -// const idToUse = getIdFromInput(input); -// const reason = interaction.options.getString('reason') ?? ''; - -// const hours = (interaction.options.getNumber('hours') ?? 0) * 3.6e+6; -// const minutes = (interaction.options.getNumber('minutes') ?? 0) * 60000; -// const seconds = (interaction.options.getNumber('seconds') ?? 0) * 1000; - -// const duration = hours + minutes + seconds; - -// const deleteMessageDays = (interaction.options.getNumber('delete-message-days') ?? 0) * 86400; - -// const fullBan = interaction.options.getBoolean('full-ban') ?? false; - -// let banMember: GuildMember | null = null; - -// try { -// banMember = await interaction.guild.members.fetch(idToUse); -// } catch (e) { -// // Member not in server. -// } - -// const member = await interaction.guild.members.fetch(interaction.user.id); - -// if (banMember && member.roles.highest.comparePositionTo(banMember.roles.highest) <= 0) { -// return await interaction.editReply('You do not have permission to ban this user.'); -// } - -// if (banMember && !banMember.bannable) { -// return await interaction.editReply('I do not have permission to ban this user.'); -// } - -// const expiresAt = new Date(Date.now() + duration); - -// await Database.putBan(idToUse, duration > 0 ? expiresAt : null, fullBan); - -// try { -// await interaction.guild.bans.create(idToUse, { -// reason: (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${interaction.user.username} (${interaction.user.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim(), -// deleteMessageSeconds: deleteMessageDays -// }); -// } catch (e) { -// console.error(e); -// return await interaction.editReply("Error banning user (couldn't ban)."); -// } - -// if (fullBan) { -// const alts = await comprehensiveAltLookupFromDiscord(idToUse, interaction.guild); - -// await removeAllAlts([alts], interaction.guild, interaction.user, fullBan, reason, deleteMessageDays, duration, expiresAt); -// } - -// await interaction.editReply(`<@${idToUse}> (${idToUse}) has been ${fullBan ? 'full banned' : 'banned'}.`); -// } -// }; - -// async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, fullBan: boolean, reason: string, deleteMessageDays: number, duration: number, expiresAt: Date) { -// for (const data of altData) { -// if (data.type == 'discord') { -// try { -// if (!data.banned) { -// await guild.members.kick(data.thisId as string, (reason + ` ${fullBan ? 'Full banned' : 'Banned'} by ${moderator.username} (${moderator.id})${duration > 0 ? `. Expires at: ${time(expiresAt, TimestampStyles.ShortDateTime)}` : ''}`).trim()); -// } -// } catch (e) { -// console.error(e); -// } -// } - -// await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); -// } -// } - // External Imports import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; +// Internal Imports +import { Database } from '../../shared/Database'; +import { CreateDefaultEmbed, SetSeverity } from '../../utils'; + // --------------- @SlashCommand( -- 2.52.0 From 0da0349994c804e985c127eb2de05ddb029f680f Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:34:04 +0800 Subject: [PATCH 09/14] (rewrite): Add `/about` command. --- src/commands/utilities/about.ts | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/commands/utilities/about.ts diff --git a/src/commands/utilities/about.ts b/src/commands/utilities/about.ts new file mode 100644 index 0000000..b41c93a --- /dev/null +++ b/src/commands/utilities/about.ts @@ -0,0 +1,42 @@ +// External Imports +import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// Internal Imports +import { CreateDefaultEmbed } from '../../utils'; + +// --------------- + +@SlashCommand( + new CommandBuilder('about', 'View information about Hexerade!') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) +) +class AboutCommand extends Command { + cooldown = 60; + + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild()) return; + + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + + description: "I'm the very cool bot for the e621 Discord.", + + fields: [ + { name: 'Members', value: `${interaction.guild.memberCount}`, inline: true }, + { name: 'Boosts (Tier)', value: `${interaction.guild.premiumSubscriptionCount} (${interaction.guild.premiumTier})`, inline: true }, + + { name: 'Developer', value: 'tarrgon.', inline: false }, + { name: 'Contributor', value: 'nxbat', inline: false }, + + { name: 'Terms of Use', value: 'https://e621.net/terms_of_use', inline: false }, + { name: 'Privacy Policy', value: 'https://e621.net/static/privacy/discordbot', inline: false }, + ] + }] + }); + } +} + +export default new AboutCommand('about'); \ No newline at end of file -- 2.52.0 From 34e88d9a4fc97d1cefa69fc0153e0e2d86924c2b Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:44:30 +0800 Subject: [PATCH 10/14] (rewrite): Updated bot to actually load the commands and events. --- src/bot.ts | 14 ++++++++++++++ src/commands/index.ts | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/commands/index.ts diff --git a/src/bot.ts b/src/bot.ts index a8fe299..6c94f63 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -3,6 +3,8 @@ import { CommandClient, Constants, LRUCollection, Member, User } from "athena-pr // Internal Imports import config from "./config"; +import events from "./events"; +import commands from "./commands"; // --------------- @@ -21,12 +23,24 @@ const client = new CommandClient({ largeBotOptimizations: true, + disableEvents: { + PRESENCE_UPDATE: true, + }, + // TODO: Move caching to Redis. cache: { users: () => new LRUCollection(User, 1024), members: (guild) => new LRUCollection(Member, 1024), + + guildEmojis: false, + guildStickers: false, + transientMembers: false, + userSweepIntervalMs: 3_600_000 // 1hr. } } }); +commands.forEach(command => client.registerCommand(command)); +events.forEach(event => client.registerEvent(event)); + export default client; \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..fd8489c --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,22 @@ +import database from "./admin/database"; +import renameChannel from "./admin/rename-channel"; + +import ban from "./moderation/ban"; +import hardban from "./moderation/hardban"; + +import about from "./utilities/about"; +import nameSync from "./utilities/name-sync"; + +export default [ + // Admin + database, + renameChannel, + + // Moderation + ban, + hardban, + + // Utilities + about, + nameSync +]; \ No newline at end of file -- 2.52.0 From dfaeee78e807731b2e1b916d7ecd6e15497193dd Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:47:20 +0800 Subject: [PATCH 11/14] (rewrite): Commands now deploy. --- src/events/index.ts | 7 ++++--- src/events/ready.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/events/ready.ts diff --git a/src/events/index.ts b/src/events/index.ts index 62133c6..6161f63 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,12 +1,13 @@ -export * from './handle-audit-log-create'; +// export * from './handle-audit-log-create'; import guildBanRemove from './guildBanRemove'; import guildCreate from './guildCreate'; import guildMemberAdd from './guildMemberAdd'; -export * from './handle-message'; +import ready from './ready'; +// export * from './handle-message'; import threadCreate from './threadCreate'; import voiceChannelJoin from './voiceChannelJoin'; import voiceChannelLeft from './voiceChannelLeft'; import voiceChannelSwitch from './voiceChannelSwitch'; -export default [guildBanRemove, guildCreate, guildMemberAdd, threadCreate, voiceChannelJoin, voiceChannelLeft, voiceChannelSwitch]; \ No newline at end of file +export default [guildBanRemove, guildCreate, guildMemberAdd, ready, threadCreate, voiceChannelJoin, voiceChannelLeft, voiceChannelSwitch]; \ No newline at end of file diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 0000000..102b0a6 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,17 @@ +// External Imports +import { CommandClient, Event, Guild } from 'athena-prime'; + +// --------------- + +class ReadyEvent extends Event { + event: string = 'ready' as const; + + async handle(context: CommandClient, guild: Guild) { + await context.deployCommands(); + + // :3 - This is non-negotiable. + context.setCustomActivity('Hugging my favourite bat: nix :3'); + } +} + +export default new ReadyEvent('ready'); \ No newline at end of file -- 2.52.0 From f4f205cf43e94eb3e5696d8a12bc7856af897219 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:52:19 +0800 Subject: [PATCH 12/14] (rewrite): About now uses the bot's avatar as the thumbnail. --- src/commands/utilities/about.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/utilities/about.ts b/src/commands/utilities/about.ts index b41c93a..d04f0a5 100644 --- a/src/commands/utilities/about.ts +++ b/src/commands/utilities/about.ts @@ -33,7 +33,11 @@ class AboutCommand extends Command { { name: 'Terms of Use', value: 'https://e621.net/terms_of_use', inline: false }, { name: 'Privacy Policy', value: 'https://e621.net/static/privacy/discordbot', inline: false }, - ] + ], + + thumbnail: { + url: context.user.avatarURL + }, }] }); } -- 2.52.0 From d6f1586f5d44a5d0077f7b531fa63a4725047005 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Tue, 11 Aug 2026 19:54:39 +0800 Subject: [PATCH 13/14] (rewrite): Remove unused dependencies. --- package-lock.json | 326 +--------------------------------------------- package.json | 2 - 2 files changed, 6 insertions(+), 322 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae41f49..d3aaf17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,9 @@ "@redis/client": "5.1.0", "athena-prime": "4.2.0", "body-parser": "2.2.2", - "discord.js": "14.26.4", "dotenv": "16.5.0", "express": "5.1.0", "express-session": "1.19.0", - "ioredis": "6.0.0", "markdown-it": "14.2.0", "memorystore": "1.6.7", "sqlite": "5.1.1", @@ -38,145 +36,6 @@ "integrity": "sha512-efrxWJ6ONQ6kcCLwPfoIonrMznvYYm9fZ1v+DgDY5BzQ8EDrgbCJr3ri9dVS7VUt9Rh8yZTXtaMDI1jf3Gb0Aw==", "license": "UNLICENSED" }, - "node_modules/@discordjs/builders": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", - "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.2", - "@discordjs/util": "^1.2.0", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.40", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", - "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.33" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", - "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.2.0", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.5", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.40", - "magic-bytes.js": "^1.13.0", - "tslib": "^2.6.3", - "undici": "6.24.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", - "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@discordjs/util": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", - "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.33" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", - "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.5.1", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.38.1", - "tslib": "^2.6.2", - "ws": "^8.17.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -825,12 +684,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@ioredis/commands": { - "version": "2.0.0", - "resolved": "https://npm.hep.gg/@ioredis/commands/-/commands-2.0.0.tgz", - "integrity": "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==", - "license": "MIT" - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -906,39 +759,6 @@ "node": ">= 18" } }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, "node_modules/@stylistic/eslint-plugin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz", @@ -1038,6 +858,7 @@ "version": "22.15.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "dev": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1075,15 +896,6 @@ "@types/send": "*" } }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", @@ -1291,16 +1103,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", - "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, "node_modules/abbrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", @@ -1844,15 +1646,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://npm.hep.gg/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1878,33 +1671,6 @@ "scripts/actions/documentation" ] }, - "node_modules/discord.js": { - "version": "14.26.4", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", - "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.14.1", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.2", - "@discordjs/rest": "^2.6.1", - "@discordjs/util": "^1.2.0", - "@discordjs/ws": "^1.2.3", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.40", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.13.0", - "tslib": "^2.6.3", - "undici": "6.24.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -2341,7 +2107,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.3.3", @@ -2799,36 +2566,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ioredis": { - "version": "6.0.0", - "resolved": "https://npm.hep.gg/ioredis/-/ioredis-6.0.0.tgz", - "integrity": "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "2.0.0", - "cluster-key-slot": "1.1.1", - "debug": "4.4.3", - "denque": "2.1.0", - "redis-errors": "1.2.0", - "standard-as-callback": "2.1.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/ioredis/node_modules/cluster-key-slot": { - "version": "1.1.1", - "resolved": "https://npm.hep.gg/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", - "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2991,29 +2728,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" - }, - "node_modules/magic-bytes.js": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", - "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", - "license": "MIT" - }, "node_modules/markdown-it": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", @@ -3747,15 +3467,6 @@ "node": ">= 6" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://npm.hep.gg/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4201,12 +3912,6 @@ } } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://npm.hep.gg/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4491,18 +4196,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", @@ -4610,19 +4303,11 @@ "node": ">= 0.8" } }, - "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true }, "node_modules/unpipe": { "version": "1.0.0", @@ -4787,6 +4472,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "optional": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 536de8d..33839b4 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,8 @@ "dependencies": { "@clynamic/dmark": "0.0.11", "@redis/client": "5.1.0", - "ioredis": "6.0.0", "athena-prime": "4.2.0", "body-parser": "2.2.2", - "discord.js": "14.26.4", "dotenv": "16.5.0", "express": "5.1.0", "express-session": "1.19.0", -- 2.52.0 From 97ac053c6963f9d51f6bb4c8d18f0ecb115b2cce Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Wed, 12 Aug 2026 16:01:53 +0800 Subject: [PATCH 14/14] (rewrite): `/cite` command rewritten. (Never even tried this yet.) --- src/commands/cite.ts | 91 +++++++++++++++++------------ src/commands/utilities/about.ts | 2 +- src/commands/utilities/name-sync.ts | 2 +- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/commands/cite.ts b/src/commands/cite.ts index c4c03bb..fe1d1cf 100644 --- a/src/commands/cite.ts +++ b/src/commands/cite.ts @@ -1,48 +1,63 @@ -import { ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +// External Imports +import { AutocompleteInteraction, Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime'; + +// Internal Imports import { Database } from '../shared/Database'; -import { KnowledgebaseItem } from '../types'; +import { CreateDefaultEmbed, SetSeverity } from '../utils'; -export default { - name: 'cite', - data: new SlashCommandBuilder() - .setName('cite') - .setDescription('Cite content from the knowledgebase.') - .setIntegrationTypes(ApplicationIntegrationType.GuildInstall) - .setContexts(InteractionContextType.Guild) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addIntegerOption(option => - option - .setName('name') - .setDescription('The name of the entry.') - .setRequired(true) - .setAutocomplete(true) - ), - handler: async function (client: Client, interaction: ChatInputCommandInteraction) { - if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' }); +// --------------- - await interaction.deferReply(); +@SlashCommand( + new CommandBuilder('cite', 'Cite a knowledgebase article.') + .setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall) + .setContexts(Constants.InteractionContextType.Guild) + .setCommandType(Constants.ApplicationCommandType.ChatInput) + .addIntegerOption({ name: 'article', description: 'The article to cite.', required: true, autocomplete: true }) +) +class CiteCommand extends Command { + cooldown = 5; // 5s. - const id = interaction.options.getInteger('name', true); + async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { + if (!interaction.inGuild()) return; + await interaction.defer(); - const item = await Database.getFromKnowledgebase(id); + const article = interaction.getRequiredInteger('article'); + const item = await Database.getFromKnowledgebase(article); + + if (!item) { + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), + ...SetSeverity('error'), - if (!item) return interaction.editReply('Knowledgebase item not found.'); + description: 'There was an issue locating the requested knowledgebase article.' + }] + }); - return interaction.editReply(item.content); - }, - autoComplete: async function (client: Client, interaction: AutocompleteInteraction) { - if (!interaction.guild) return interaction.respond([]); + return; + } - const items: KnowledgebaseItem[] = await Database.getAllKnowledgebaseItems(interaction.guild.id); + await interaction.createMessage({ + embeds: [{ + ...CreateDefaultEmbed(context), - const value = interaction.options.getFocused(); - - const toRespond = items.filter(i => !value ? true : i.content.includes(value)); - if (toRespond.length > 25) toRespond.length = 25; - - interaction.respond(toRespond.map(p => ({ - name: p.name, - value: p.id - }))); + description: item.content + }] + }); } -}; \ No newline at end of file + + async handleAutocomplete(context: CommandClient, interaction: AutocompleteInteraction, data: any) { + if (!interaction.guild) return await interaction.acknowledge([]); + const items = await Database.getAllKnowledgebaseItems(interaction.guild.id); + + const value = interaction.focused(); + if(!value) return await interaction.acknowledge([]); + + const response = items.filter(item => item.content.includes(value.value as string)); + if (response.length > 25) response.length = 25; + + return await interaction.acknowledge(response.map(res => ({ name: res.name, value: res.id }))); + } +} + +export default new CiteCommand('cite'); \ No newline at end of file diff --git a/src/commands/utilities/about.ts b/src/commands/utilities/about.ts index d04f0a5..6947a9d 100644 --- a/src/commands/utilities/about.ts +++ b/src/commands/utilities/about.ts @@ -13,7 +13,7 @@ import { CreateDefaultEmbed } from '../../utils'; .setCommandType(Constants.ApplicationCommandType.ChatInput) ) class AboutCommand extends Command { - cooldown = 60; + cooldown = 5; // 5s. async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { if (!interaction.inGuild()) return; diff --git a/src/commands/utilities/name-sync.ts b/src/commands/utilities/name-sync.ts index e7531a9..76b4498 100644 --- a/src/commands/utilities/name-sync.ts +++ b/src/commands/utilities/name-sync.ts @@ -25,7 +25,7 @@ async function fetchUser(member: Member, id: number | null): Promise { - cooldown = 60; + cooldown = 360; // 5m. async handleCommand(context: CommandClient, interaction: CommandInteraction, data: any) { if (!interaction.inGuild()) return; -- 2.52.0