From 53a658aa696458fb6e5efb061c1ef9eddbd0a31e Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:36:15 -0400 Subject: [PATCH 1/4] Add appeals pubsub events --- src/shared/Database.ts | 27 +++++- src/shared/RedisClient.ts | 6 +- src/types/database-types.d.ts | 8 +- src/types/e621-types.d.ts | 18 ++++ src/utils/appeal-events.ts | 102 +++++++++++++++++++++ src/utils/event-utils.ts | 163 ++++++++++++++++++++++++++++++++++ src/utils/index.ts | 1 + src/utils/ticket-events.ts | 163 +--------------------------------- 8 files changed, 322 insertions(+), 166 deletions(-) create mode 100644 src/utils/appeal-events.ts create mode 100644 src/utils/event-utils.ts diff --git a/src/shared/Database.ts b/src/shared/Database.ts index 71bc95a..25eabee 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -1,7 +1,7 @@ import sqlite3 from 'sqlite3'; import { open, Database as SqliteDatabase } from 'sqlite'; import { serializeMessage, wait } from '../utils'; -import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket } from '../types'; +import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket, AppealMessage } from '../types'; import { Message } from '../events'; const DB_SCHEMA = ` @@ -30,7 +30,8 @@ const DB_SCHEMA = ` link_skip_channels TEXT, github_release_channel TEXT, moderator_channel_id TEXT, - private_help_channel_id TEXT + private_help_channel_id TEXT, + appeals_channel_id TEXT ); CREATE TABLE IF NOT EXISTS messages ( @@ -58,6 +59,11 @@ const DB_SCHEMA = ` phrase TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS appeals ( + id INTEGER PRIMARY KEY, + message_id TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY, user_id TEXT, @@ -354,6 +360,23 @@ export class Database { // -- END TICKETS -- + // -- START APPEALS -- + + static async putAppeal(appealId: number, messageId: string) { + await Database.db.run('INSERT INTO appeals(id, message_id) VALUES (?, ?)', appealId, messageId); + } + + static async removeAppeal(appealId: number) { + await Database.db.run('DELETE from appeals WHERE id = ?', appealId); + } + + static async getAppealMessageId(appealId: number): Promise { + const appeal = await Database.db.get>('SELECT message_id FROM appeals WHERE id = ?', appealId); + return appeal?.message_id; + } + + // -- END APPEALS -- + // -- START NOTES -- static async putNote(userId: string, reason: string, modId: string) { diff --git a/src/shared/RedisClient.ts b/src/shared/RedisClient.ts index dcd317b..32ebb2a 100644 --- a/src/shared/RedisClient.ts +++ b/src/shared/RedisClient.ts @@ -1,6 +1,6 @@ import { createClient, SocketClosedUnexpectedlyError } from '@redis/client'; import { Client } from 'discord.js'; -import { banUpdateHandler, ticketUpdateHandler } from '../utils'; +import { banUpdateHandler, ticketUpdateHandler, appealUpdateHandler } from '../utils'; let discordClient: Client; @@ -32,7 +32,7 @@ export async function openRedisClient(url: string, discClient: Client) { }); client.once('connect', () => { - client.subscribe(['ticket_updates', 'ban_updates'], updateHandler); + client.subscribe(['ticket_updates', 'ban_updates', 'appeal_updates'], updateHandler); }); client.connect(); @@ -46,5 +46,7 @@ function updateHandler(data: string, channel: string) { return ticketUpdateHandler(discordClient, data); case 'ban_updates': return banUpdateHandler(discordClient, data); + case 'appeal_updates': + return appealUpdateHandler(discordClient, data); } } \ No newline at end of file diff --git a/src/types/database-types.d.ts b/src/types/database-types.d.ts index 2d078da..27013eb 100644 --- a/src/types/database-types.d.ts +++ b/src/types/database-types.d.ts @@ -25,7 +25,8 @@ export type GuildSettings = { link_skip_channels?: string github_release_channel?: string moderator_channel_id?: string - private_help_channel_id?: string + private_help_channel_id?: string, + appeals_channel_id?: string } export type GuildArraySetting = 'staff_categories' | 'safe_channels' | 'link_skip_channels'; @@ -35,6 +36,11 @@ export type TicketMessage = { message_id: string } +export type AppealMessage = { + id: number + message_id: string +} + export type TicketPhrase = { id: number user_id: string diff --git a/src/types/e621-types.d.ts b/src/types/e621-types.d.ts index c4bc61b..e891723 100644 --- a/src/types/e621-types.d.ts +++ b/src/types/e621-types.d.ts @@ -142,6 +142,24 @@ export type BanUpdate = { ban: Ban }; +export type Appeal = { + id: number + user_id: number + user: string + claimant: string | null + target?: string + accused_id: number + target_id: number + status: 'pending' | 'partial' | 'approved' | 'rejected' + category: 'flag' + reason: string +} + +export type AppealUpdate = { + action: 'claim' | 'create' | 'unclaim' | 'update' + appeal: Appeal +}; + export type RecordCategory = 'positive' | 'negative' | 'neutral' export type Record = { diff --git a/src/utils/appeal-events.ts b/src/utils/appeal-events.ts new file mode 100644 index 0000000..29e0589 --- /dev/null +++ b/src/utils/appeal-events.ts @@ -0,0 +1,102 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder } from 'discord.js'; +import { config } from '../config'; +import { Appeal, AppealUpdate } from '../types'; +import { getAuthor, getColor, getDescription, getFields } from './event-utils'; +import { humanizeCapitalization } from './string-utils'; +import { Database } from '../shared/Database'; + +export async function appealUpdateHandler(client: Client, update: string) { + const data: AppealUpdate = JSON.parse(update); + + if (data.action == 'create') { + postAppeal(client, data); + } else { + updateAppeal(client, data); + } +} + +async function postAppeal(client: Client, data: AppealUpdate) { + const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!); + + if (!guildSettings || !guildSettings.appeals_channel_id) return; + + const channel = await client.channels.fetch(guildSettings.appeals_channel_id); + + if (!channel || !channel.isSendable()) return; + + const appeal = data.appeal; + + const embed = await createEmbedFromAppeal(appeal); + + const row = await getButtons(appeal); + + const message = await channel.send({ embeds: [embed], components: [row] }); + + await Database.putAppeal(appeal.id, message.id); +} + +async function updateAppeal(client: Client, data: AppealUpdate) { + const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!); + + if (!guildSettings || !guildSettings.tickets_channel_id) return; + + const channel = await client.channels.fetch(guildSettings.tickets_channel_id); + + if (!channel || !channel.isSendable()) return; + + const messageId = await Database.getAppealMessageId(data.appeal.id); + + if (!messageId) return postAppeal(client, data); + + const message = await channel.messages.fetch(messageId); + const embed = await createEmbedFromAppeal(data.appeal); + + if (!message || message.author.id != config.DISCORD_CLIENT_ID) { + const newMessage = await channel.send({ embeds: [embed] }); + await Database.removeAppeal(data.appeal.id); + await Database.putAppeal(data.appeal.id, newMessage.id); + } else { + await message.edit({ embeds: [embed] }); + } +} + +function getTitle(appeal: Appeal): string { + if (!appeal.target) return `${humanizeCapitalization(appeal.category)} appeal from ${appeal.user}`; + + switch (appeal.category) { + case 'flag': + return `Flag by ${appeal.target}`; + default: + return 'Uknown ticket category'; + } +} + +function getURL(appeal: Appeal): string { + return `${config.E621_BASE_URL}/appeals/${appeal.id}`; +} + +async function createEmbedFromAppeal(appeal: Appeal): Promise { + return new EmbedBuilder() + .setTitle(getTitle(appeal)) + .setURL(await getURL(appeal)) + .setDescription(await getDescription(appeal)) + .setAuthor(getAuthor(appeal)) + .setColor(getColor(appeal)) + .setFields(...getFields(appeal)) + .setFooter({ text: `Appeal #${appeal.id}` }); +} + +async function getButtons(appeal: Appeal): Promise> { + const row = new ActionRowBuilder(); + + const primaryButton = new ButtonBuilder() + .setStyle(ButtonStyle.Link); + + if (appeal.category == 'flag') { + primaryButton + .setLabel('Open Flag') + .setURL(`${config.E621_BASE_URL}/post_flags/${appeal.target_id}`); + } + + return row; +} \ No newline at end of file diff --git a/src/utils/event-utils.ts b/src/utils/event-utils.ts new file mode 100644 index 0000000..0815ed8 --- /dev/null +++ b/src/utils/event-utils.ts @@ -0,0 +1,163 @@ +import { EmbedAuthorOptions, APIEmbedField } from 'discord.js'; +import { config } from '../config'; +import { getE621Post, spoilerOrBlacklist, PostAction } from './e621-utils'; +import { blipIDRegex, commentIDRegex, forumTopicIDRegex, poolIDRegex, postIDRegex, recordIDRegex, searchLinkRegex, setIDRegex, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from './message-matcher-regex'; + +// TODO: Condense this and the message event handler regex array. +const linkReplacers = [ + { + regex: blipIDRegex, + replacement: '/blips/{match}', + encodeURI: false + }, + { + regex: commentIDRegex, + replacement: '/comments/{match}', + encodeURI: false + }, + { + regex: forumTopicIDRegex, + replacement: '/forum_topics/{match}', + encodeURI: false + }, + { + regex: poolIDRegex, + replacement: '/pools/{match}', + encodeURI: false + }, + { + regex: postIDRegex, + tester: async (postId: string, before: string, after: string) => { + const post = await getE621Post(postId); + if (!post) return { allowed: true, before, after }; + const allowed = spoilerOrBlacklist(post).action != PostAction.Blacklist; + + return { allowed, before, after }; + }, + replacement: '/posts/{match}', + encodeURI: false + }, + { + regex: recordIDRegex, + replacement: '/user_feedbacks/{match}', + encodeURI: false + }, + { + regex: searchLinkRegex, + replacement: '/posts?tags={match}', + encodeURI: true + }, + { + regex: setIDRegex, + replacement: '/post_sets/{match}', + encodeURI: false + }, + { + regex: takedownIDRegex, + replacement: '/takedowns/{match}', + encodeURI: false + }, + { + regex: ticketIDRegex, + replacement: '/tickets/{match}', + encodeURI: false + }, + { + regex: userIDRegex, + replacement: '/users/{match}', + encodeURI: false + }, + { + regex: wikiLinkRegex, + replacement: '/wiki_pages/{match}', + encodeURI: true + } +]; + +const urlRegex = new RegExp('"((?:[\\S]| )+?)":\\[?((?:https?:\\/\\/[\\w\\d.\\/?=#&%]+)|\\/[\\w\\d.\\/?=#\\[\\]]+)\\]?', 'gi'); + +const MAX_DESCRIPTION_LENGTH = 500; + +export async function getLinks(input: string, limit: number = Number.MAX_SAFE_INTEGER): Promise { + const length = input.length; + + const replacedIndexes: { start: number, end: number }[] = []; + const checks: Promise<{ allowed: boolean, before: string, after: string }>[] = []; + + for (const replacer of linkReplacers) { + input = input.replaceAll(replacer.regex, (match, group1) => { + const replaced = `[${match}](${config.E621_BASE_URL}${(replacer.replacement).replace('{match}', replacer.encodeURI ? encodeURIComponent(group1) : group1)})`; + if (replacer.tester) checks.push(replacer.tester(group1, match, replaced)); + const start = input.indexOf(match); + replacedIndexes.push({ start, end: start + replaced.length }); + + return replaced; + }); + } + + input = input.replaceAll(urlRegex, (match, group1, group2) => { + const replaced = group2.startsWith('/') ? `[${group1}](${config.E621_BASE_URL}${group2})` : `[${group1}](${group2})`; + const start = input.indexOf(match); + replacedIndexes.push({ start, end: start + replaced.length }); + return replaced; + }); + + const values = await Promise.all(checks); + + for (const check of values) { + if (!check.allowed) { + input = input.replace(check.after, check.before); + } + } + + if (length > limit) { + for (const replacedIndex of replacedIndexes) { + if (replacedIndex.start < limit && replacedIndex.end >= limit) { + return input.substring(0, replacedIndex.end) + '...'; + } + } + + return input.substring(0, limit) + '...'; + } + + return input; +} + +export async function getDescription(data: { reason: string }): Promise { + return data.reason.length <= MAX_DESCRIPTION_LENGTH ? await getLinks(data.reason) : await getLinks(data.reason, MAX_DESCRIPTION_LENGTH); +} + +export function getAuthor(data: { user_id: number, user: string }): EmbedAuthorOptions { + return { + url: `${config.E621_BASE_URL}/users/${data.user_id}`, + name: data.user + }; +} + +export function getColor(data: { claimant: string | null }): number { + if (!data.claimant) { + return 0xff0000; + } else { + return 0x00ffff; + } +} + +export function getFields(data: { category: string, status: string, claimant: string | null }): APIEmbedField[] { + return [ + { + name: 'Type', + value: data.category, + inline: true + }, + { + name: 'Status', + value: data.status, + inline: true + }, + { + name: 'Claimed By', + value: !data.claimant ? '' : data.claimant, + inline: true + } + ]; +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 8d9a9a6..275420d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './alt-utils'; +export * from './appeal-events'; export * from './array-utils'; export * from './audit-log-utils'; export * from './ban-events'; diff --git a/src/utils/ticket-events.ts b/src/utils/ticket-events.ts index bf28059..fd33e75 100644 --- a/src/utils/ticket-events.ts +++ b/src/utils/ticket-events.ts @@ -1,87 +1,12 @@ -import { APIEmbedField, ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedAuthorOptions, EmbedBuilder, SendableChannels } from 'discord.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, SendableChannels } from 'discord.js'; import { config } from '../config'; import { Database } from '../shared/Database'; import { Ticket, TicketPhrase, TicketUpdate } from '../types'; import { PostAction, getE621Post, getE621User, spoilerOrBlacklist } from './e621-utils'; -import { blipIDRegex, commentIDRegex, forumTopicIDRegex, poolIDRegex, postIDRegex, recordIDRegex, searchLinkRegex, setIDRegex, takedownIDRegex, ticketIDRegex, userIDRegex, wikiLinkRegex } from './message-matcher-regex'; +import { getAuthor, getColor, getDescription, getFields } from './event-utils'; import { humanizeCapitalization } from './string-utils'; import { shouldAlert } from './ticket-utils'; -// TODO: Condense this and the message event handler regex array. -const linkReplacers = [ - { - regex: blipIDRegex, - replacement: '/blips/{match}', - encodeURI: false - }, - { - regex: commentIDRegex, - replacement: '/comments/{match}', - encodeURI: false - }, - { - regex: forumTopicIDRegex, - replacement: '/forum_topics/{match}', - encodeURI: false - }, - { - regex: poolIDRegex, - replacement: '/pools/{match}', - encodeURI: false - }, - { - regex: postIDRegex, - tester: async (postId: string, before: string, after: string) => { - const post = await getE621Post(postId); - if (!post) return { allowed: true, before, after }; - const allowed = spoilerOrBlacklist(post).action != PostAction.Blacklist; - - return { allowed, before, after }; - }, - replacement: '/posts/{match}', - encodeURI: false - }, - { - regex: recordIDRegex, - replacement: '/user_feedbacks/{match}', - encodeURI: false - }, - { - regex: searchLinkRegex, - replacement: '/posts?tags={match}', - encodeURI: true - }, - { - regex: setIDRegex, - replacement: '/post_sets/{match}', - encodeURI: false - }, - { - regex: takedownIDRegex, - replacement: '/takedowns/{match}', - encodeURI: false - }, - { - regex: ticketIDRegex, - replacement: '/tickets/{match}', - encodeURI: false - }, - { - regex: userIDRegex, - replacement: '/users/{match}', - encodeURI: false - }, - { - regex: wikiLinkRegex, - replacement: '/wiki_pages/{match}', - encodeURI: true - } -]; - -const urlRegex = new RegExp('"((?:[\\S]| )+?)":\\[?((?:https?:\\/\\/[\\w\\d.\\/?=#&%]+)|\\/[\\w\\d.\\/?=#\\[\\]]+)\\]?', 'gi'); - -const MAX_DESCRIPTION_LENGTH = 500; - export async function ticketUpdateHandler(client: Client, update: string) { const data: TicketUpdate = JSON.parse(update); @@ -170,90 +95,6 @@ function getURL(ticket: Ticket): string { return `${config.E621_BASE_URL}/tickets/${ticket.id}`; } -async function getLinks(input: string, limit: number = Number.MAX_SAFE_INTEGER): Promise { - const length = input.length; - - const replacedIndexes: { start: number, end: number }[] = []; - const checks: Promise<{ allowed: boolean, before: string, after: string }>[] = []; - - for (const replacer of linkReplacers) { - input = input.replaceAll(replacer.regex, (match, group1) => { - const replaced = `[${match}](${config.E621_BASE_URL}${(replacer.replacement).replace('{match}', replacer.encodeURI ? encodeURIComponent(group1) : group1)})`; - if (replacer.tester) checks.push(replacer.tester(group1, match, replaced)); - const start = input.indexOf(match); - replacedIndexes.push({ start, end: start + replaced.length }); - - return replaced; - }); - } - - input = input.replaceAll(urlRegex, (match, group1, group2) => { - const replaced = group2.startsWith('/') ? `[${group1}](${config.E621_BASE_URL}${group2})` : `[${group1}](${group2})`; - const start = input.indexOf(match); - replacedIndexes.push({ start, end: start + replaced.length }); - return replaced; - }); - - const values = await Promise.all(checks); - - for (const check of values) { - if (!check.allowed) { - input = input.replace(check.after, check.before); - } - } - - if (length > limit) { - for (const replacedIndex of replacedIndexes) { - if (replacedIndex.start < limit && replacedIndex.end >= limit) { - return input.substring(0, replacedIndex.end) + '...'; - } - } - - return input.substring(0, limit) + '...'; - } - - return input; -} - -async function getDescription(ticket: Ticket): Promise { - return ticket.reason.length <= MAX_DESCRIPTION_LENGTH ? await getLinks(ticket.reason) : await getLinks(ticket.reason, MAX_DESCRIPTION_LENGTH); -} - -function getAuthor(ticket: Ticket): EmbedAuthorOptions { - return { - url: `${config.E621_BASE_URL}/users/${ticket.user_id}`, - name: ticket.user - }; -} - -function getColor(ticket: Ticket): number { - if (!ticket.claimant) { - return 0xff0000; - } else { - return 0x00ffff; - } -} - -function getFields(ticket: Ticket): APIEmbedField[] { - return [ - { - name: 'Type', - value: ticket.category, - inline: true - }, - { - name: 'Status', - value: ticket.status, - inline: true - }, - { - name: 'Claimed By', - value: !ticket.claimant ? '' : ticket.claimant, - inline: true - } - ]; -} - async function createEmbedFromTicket(ticket: Ticket): Promise { return new EmbedBuilder() .setTitle(getTitle(ticket)) From e6fb923531c5dd25e04cd737e3060093582571e1 Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:40:52 -0400 Subject: [PATCH 2/4] Settings --- src/commands/settings.ts | 14 ++++++++++++++ src/shared/Database.ts | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 658ac4f..fed3834 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -21,6 +21,12 @@ export default { .setDescription('Set the ticket logs channel.') .setRequired(false) ) + .addChannelOption(option => + option + .setName('appeals-channel') + .setDescription('Set the appeal logs channel.') + .setRequired(false) + ) .addChannelOption(option => option .setName('event-logs-channel') @@ -144,6 +150,14 @@ export default { response += `Tickets logs channel set to ${ticketsChannel}.\n`; } + const appealsChannel = interaction.options.getChannel('appeals-channel'); + + if (appealsChannel) { + await Database.setGuildAppealsLogsChannelId(interaction.guildId!, appealsChannel.id); + + response += `Appeals logs channel set to ${appealsChannel}.\n`; + } + const eventLogsChannel = interaction.options.getChannel('event-logs-channel'); if (eventLogsChannel) { diff --git a/src/shared/Database.ts b/src/shared/Database.ts index 25eabee..6808d86 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -198,6 +198,10 @@ export class Database { await Database.db.run('UPDATE settings SET tickets_channel_id = ? WHERE guild_id = ?', id, guildId); } + static async setGuildAppealsLogsChannelId(guildId: string, id: string) { + await Database.db.run('UPDATE settings SET appeals_channel_id = ? WHERE guild_id = ?', id, guildId); + } + static async setGuildEventsLogsChannelId(guildId: string, id: string) { await Database.db.run('UPDATE settings SET event_logs_channel_id = ? WHERE guild_id = ?', id, guildId); } From 8a958ed326b5bf7c444d109f96617d61e6ad60f9 Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:35:45 -0400 Subject: [PATCH 3/4] Switch to migrations --- src/migrations/0001-add-appeals.sql | 16 ++++++++++++++++ src/shared/Database.ts | 27 +++++++++++++++++---------- 2 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 src/migrations/0001-add-appeals.sql diff --git a/src/migrations/0001-add-appeals.sql b/src/migrations/0001-add-appeals.sql new file mode 100644 index 0000000..cd25dae --- /dev/null +++ b/src/migrations/0001-add-appeals.sql @@ -0,0 +1,16 @@ +-------------------------------------------------------------------------------- +-- Up +-------------------------------------------------------------------------------- +ALTER TABLE settings ADD appeals_channel_id TEXT; + +CREATE TABLE appeals ( + id INTEGER PRIMARY KEY, + message_id TEXT NOT NULL +); + +-------------------------------------------------------------------------------- +-- Down +-------------------------------------------------------------------------------- +ALTER TABLE settings DROP COLUMN appeals_channel_id; + +DROP TABLE appeals; \ No newline at end of file diff --git a/src/shared/Database.ts b/src/shared/Database.ts index 6808d86..0fcb4e3 100644 --- a/src/shared/Database.ts +++ b/src/shared/Database.ts @@ -1,8 +1,9 @@ -import sqlite3 from 'sqlite3'; +import path from 'path'; import { open, Database as SqliteDatabase } from 'sqlite'; -import { serializeMessage, wait } from '../utils'; -import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Note, Ban, GuildArraySetting, GithubUserMapping, KnowledgebaseItem, PrivateHelpTicket, AppealMessage } from '../types'; +import sqlite3 from 'sqlite3'; import { Message } from '../events'; +import { AppealMessage, Ban, GithubUserMapping, GuildArraySetting, GuildSettings, KnowledgebaseItem, LoggedMessage, Note, PrivateHelpTicket, TicketMessage, TicketPhrase } from '../types'; +import { serializeMessage, wait } from '../utils'; const DB_SCHEMA = ` CREATE TABLE IF NOT EXISTS discord_names ( @@ -30,8 +31,7 @@ const DB_SCHEMA = ` link_skip_channels TEXT, github_release_channel TEXT, moderator_channel_id TEXT, - private_help_channel_id TEXT, - appeals_channel_id TEXT + private_help_channel_id TEXT ); CREATE TABLE IF NOT EXISTS messages ( @@ -59,11 +59,6 @@ const DB_SCHEMA = ` phrase TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS appeals ( - id INTEGER PRIMARY KEY, - message_id TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY, user_id TEXT, @@ -134,6 +129,8 @@ export class Database { console.log('SQLite database opened'); await Database.ensure(); + + await Database.migrate(); } private static async ensure() { @@ -141,6 +138,16 @@ export class Database { console.log('SQLite database ensured'); } + private static async migrate() { + console.log('Starting database migrations'); + + await Database.db.migrate({ + migrationsPath: path.join(__dirname, '..', 'migrations') + }); + + console.log('Database migrations ran'); + } + // -- START WHOIS -- static async getE621Ids(discordId: string): Promise { From 86911ad141af593c538e1aac1a754c0536901305 Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:08:13 -0400 Subject: [PATCH 4/4] Fixes --- src/utils/appeal-events.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/appeal-events.ts b/src/utils/appeal-events.ts index 29e0589..db3cd47 100644 --- a/src/utils/appeal-events.ts +++ b/src/utils/appeal-events.ts @@ -38,9 +38,9 @@ async function postAppeal(client: Client, data: AppealUpdate) { async function updateAppeal(client: Client, data: AppealUpdate) { const guildSettings = await Database.getGuildSettings(config.DISCORD_GUILD_ID!); - if (!guildSettings || !guildSettings.tickets_channel_id) return; + if (!guildSettings || !guildSettings.appeals_channel_id) return; - const channel = await client.channels.fetch(guildSettings.tickets_channel_id); + const channel = await client.channels.fetch(guildSettings.appeals_channel_id); if (!channel || !channel.isSendable()) return; @@ -67,7 +67,7 @@ function getTitle(appeal: Appeal): string { case 'flag': return `Flag by ${appeal.target}`; default: - return 'Uknown ticket category'; + return 'Uknown appeal category'; } } @@ -98,5 +98,7 @@ async function getButtons(appeal: Appeal): Promise