(rewrite): Bot & Events. This most definitely doesn't work yet. I'm just running out of time to work on this atm.

This commit is contained in:
2026-08-11 10:27:07 +08:00
parent 2b65793903
commit 32b06a384b
25 changed files with 292 additions and 154 deletions
+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}.`);
}
+5 -10
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,
@@ -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`);
}
}
};
+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
});
}
}
}