WIP: [#1]: Athena Rewrite #1

Draft
nxbat wants to merge 14 commits from athena-rewrite into dev
25 changed files with 292 additions and 154 deletions
Showing only changes of commit 32b06a384b - Show all commits
+1
View File
@@ -1 +1,2 @@
registry=https://npm.hep.gg
@clynamic:registry=https://npm.clynamic.net
+88 -5
View File
@@ -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",
+4
View File
@@ -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",
+32
View File
@@ -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;
+1 -1
View File
@@ -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.' });
}
};
+14 -14
View File
@@ -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}.`);
}
+4 -9
View File
@@ -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,
@@ -21,9 +22,3 @@ export const config = {
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`);
}
}
+17
View File
@@ -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<CommandClient> {
event: string = 'guildBanRemove' as const;
async handle(context: CommandClient<any, any>, guild: Guild, user: User) {
await Database.deleteBan(user.id);
}
}
export default new GuildBanRemoveEvent('guildBanRemove');
+15
View File
@@ -0,0 +1,15 @@
// External Imports
import { CommandClient, Event, Guild } from 'athena-prime';
// Internal Imports
import { Database } from '../shared/Database';
class GuildCreateEvent extends Event<CommandClient> {
event: string = 'guildCreate' as const;
async handle(context: CommandClient<any, any>, guild: Guild) {
await Database.GetOrCreateSettings(guild.id);
}
}
export default new GuildCreateEvent('guildCreate');
+24
View File
@@ -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<CommandClient> {
event: string = 'guildMemberAdd' as const;
async handle(context: CommandClient<any, any>, 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');
-6
View File
@@ -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);
}
-10
View File
@@ -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);
}
}
-23
View File
@@ -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);
}
}
}
}
+5 -3
View File
@@ -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];
+7 -6
View File
@@ -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<Task, NodeJS.Timeout>();
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 }
@@ -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<void> {
async handle(context: CommandClient) {
await checkExpiredBans(context);
}
}
@@ -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<void> {
async handle(context: CommandClient) {
await closeOldTickets(context);
}
}
@@ -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<void> {
async handle(context: CommandClient) {
await Database.pruneOldMessages();
}
}
+15 -12
View File
@@ -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<void> {
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<GuildSettings> {
return await Database.db.get<GuildSettings>('SELECT * FROM settings WHERE guild_id = ?', guildId) as GuildSettings;
}
static async GetOrCreateSettings(guildId: string): Promise<GuildSettings> {
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<GuildSettings>(
'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<keyof GuildSettings, 'guild_id'>, 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<Ban[]>('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));
}
-1
View File
@@ -33,7 +33,6 @@ export type GuildSettings = {
site_breaker_role_id?: string
}
type GuildSetting = Exclude<keyof GuildSettings, 'guild_id'>
export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels';
export type TicketMessage = {
+2 -2
View File
@@ -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<void>;
handle(context: CommandClient): Promise<void>;
};
+10 -18
View File
@@ -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<string> {
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<AltData> {
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;
}
+11 -11
View File
@@ -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<Ban[]>('DELETE from bans WHERE expires = 1 AND expires_at <= ?', date);
}
+7 -11
View File
@@ -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 };
+15 -16
View File
@@ -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
});
}
}
}