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 + }); } } }