feat(tickets): add ticket claiming
jump to
@@ -1,58 +0,0 @@
-CREATE TABLE `guild` ( - `guild_id` text PRIMARY KEY NOT NULL, - `suggestion_forum_id` text, - `bug_channel_id` text, - `highlights_channel_id` text, - `commands_channel_id` text, - `ticket_channel_id` text, - `ticket_category_id` text, - `ticket_message` text -); ---> statement-breakpoint -CREATE TABLE `manager` ( - `role_id` text PRIMARY KEY NOT NULL, - `guild_id` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `ticket_attachments` ( - `id` text PRIMARY KEY NOT NULL, - `ticket_id` integer NOT NULL, - `message_id` integer, - `file_name` text NOT NULL, - `content_type` text NOT NULL, - `size` integer NOT NULL, - `bucket` text NOT NULL, - `key` text NOT NULL, - `url` text NOT NULL, - `uploaded_at` integer DEFAULT (unixepoch()) NOT NULL, - FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`message_id`) REFERENCES `ticket_messages`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `ticket_messages` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `ticket_id` integer NOT NULL, - `author_id` text NOT NULL, - `author_type` text DEFAULT 'user' NOT NULL, - `content` text NOT NULL, - `created_at` integer DEFAULT (unixepoch()) NOT NULL, - FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE INDEX `ticket_msgs_ticket_idx` ON `ticket_messages` (`ticket_id`);--> statement-breakpoint -CREATE TABLE `tickets` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `guild_id` text NOT NULL, - `channel_id` text, - `author_id` text NOT NULL, - `anonymous_id` text NOT NULL, - `type` text NOT NULL, - `status` text DEFAULT 'open' NOT NULL, - `topic` text, - `created_at` integer DEFAULT (unixepoch()) NOT NULL, - `closed_at` integer -); ---> statement-breakpoint -CREATE INDEX `tickets_guild_idx` ON `tickets` (`guild_id`);--> statement-breakpoint -CREATE INDEX `tickets_author_idx` ON `tickets` (`author_id`);--> statement-breakpoint -CREATE INDEX `tickets_status_idx` ON `tickets` (`status`);
@@ -1,537 +0,0 @@
-{ - "dialect": "sqlite", - "id": "09c91ede-1e86-47c0-a78b-57795ffc3568", - "prevIds": [ - "00000000-0000-0000-0000-000000000000" - ], - "version": "7", - "ddl": [ - { - "name": "guild", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "guild_id", - "table": "guild", - "entityType": "columns" - }, - { - "columns": [ - "guild_id" - ], - "nameExplicit": false, - "name": "guild_pk", - "table": "guild", - "entityType": "pks" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "suggestion_forum_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "bug_channel_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "highlights_channel_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands_channel_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "ticket_channel_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "ticket_category_id", - "table": "guild", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "ticket_message", - "table": "guild", - "entityType": "columns" - }, - { - "name": "manager", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "role_id", - "table": "manager", - "entityType": "columns" - }, - { - "columns": [ - "role_id" - ], - "nameExplicit": false, - "name": "manager_pk", - "table": "manager", - "entityType": "pks" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "guild_id", - "table": "manager", - "entityType": "columns" - }, - { - "name": "ticket_attachments", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "ticket_attachments_pk", - "table": "ticket_attachments", - "entityType": "pks" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "ticket_id", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "file_name", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content_type", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "size", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "bucket", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "key", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())", - "generated": null, - "name": "uploaded_at", - "table": "ticket_attachments", - "entityType": "columns" - }, - { - "columns": [ - "ticket_id" - ], - "tableTo": "tickets", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "ticket_attachments_ticket_id_tickets_id_fk", - "table": "ticket_attachments", - "entityType": "fks" - }, - { - "columns": [ - "message_id" - ], - "tableTo": "ticket_messages", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "ticket_attachments_message_id_ticket_messages_id_fk", - "table": "ticket_attachments", - "entityType": "fks" - }, - { - "name": "ticket_messages", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": true, - "default": null, - "generated": null, - "name": "id", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "ticket_messages_pk", - "table": "ticket_messages", - "entityType": "pks" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "ticket_id", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "author_id", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "'user'", - "generated": null, - "name": "author_type", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())", - "generated": null, - "name": "created_at", - "table": "ticket_messages", - "entityType": "columns" - }, - { - "columns": [ - { - "value": "ticket_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "ticket_msgs_ticket_idx", - "table": "ticket_messages", - "entityType": "indexes" - }, - { - "columns": [ - "ticket_id" - ], - "tableTo": "tickets", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "ticket_messages_ticket_id_tickets_id_fk", - "table": "ticket_messages", - "entityType": "fks" - }, - { - "name": "tickets", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": true, - "default": null, - "generated": null, - "name": "id", - "table": "tickets", - "entityType": "columns" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "tickets_pk", - "table": "tickets", - "entityType": "pks" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "guild_id", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "channel_id", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "author_id", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "anonymous_id", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "'open'", - "generated": null, - "name": "status", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "topic", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "(unixepoch())", - "generated": null, - "name": "created_at", - "table": "tickets", - "entityType": "columns" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "closed_at", - "table": "tickets", - "entityType": "columns" - }, - { - "columns": [ - { - "value": "guild_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "tickets_guild_idx", - "table": "tickets", - "entityType": "indexes" - }, - { - "columns": [ - { - "value": "author_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "tickets_author_idx", - "table": "tickets", - "entityType": "indexes" - }, - { - "columns": [ - { - "value": "status", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "tickets_status_idx", - "table": "tickets", - "entityType": "indexes" - } - ], - "renames": [] -}
@@ -327,19 +327,6 @@ );
if (!hasLinkMarker && !hasWordMarker) return null; const signals = await fetchSignals(client, message); - const ONE_HOUR = 60 * 60 * 1000; - - logger.debug( - { - signals, - message, - markers: { - hasLinkMarker, - hasWordMarker, - }, - }, - "evaluating message for potential phishing markers", - ); // if ( // hasWordMarker &&
@@ -2,4 +2,5 @@ export * from "./tickets";
export * from "./guild"; export * from "./attachments"; export * from "./bugs"; +export * from "./moderators"; export * from "./userPreferences";
@@ -0,0 +1,10 @@
+import { sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const moderators = sqliteTable("moderators", { + user_id: text("user_id").primaryKey(), + subjective: text("subjective").notNull(), + objective: text("objective").notNull(), + possessiveDeterminer: text("possessive_determiner").notNull(), + possessive: text("possessive").notNull(), + reflexive: text("reflexive").notNull(), +})
@@ -11,8 +11,9 @@
channelId: text("channel_id"), authorId: text("author_id").notNull(), - anonymousId: text("anonymous_id").notNull(), + anonymousId: text("anonymous_id").notNull(), claimedBy: text("claimed_by"), + claimedAt: integer("claimed_at", { mode: "timestamp" }), addedUsers: text("added_users", { mode: "json" }) .$type<string[]>()
@@ -202,10 +202,6 @@ `Guest_${index + 1}`,
); } - logger.debug( - `Anonymised content for message ${message.id}: ${anonymisedContent}`, - ); - const savedMessage = db .insert(ticketMessages) .values({
@@ -37,10 +37,6 @@ `Guest_${index + 1}`,
); } - logger.debug( - `Anonymised content for message ${newMessage.id}: ${anonymisedContent}`, - ); - await db .update(ticketMessages) .set({ content: anonymisedContent })
@@ -16,5 +16,6 @@ logger.error(error, "unhandled promise rejection:");
}); process.on("uncaughtException", (error) => { - logger.error(error, "uncaught exception"); + logger.error(error, "uncaught exception"); + console.error(error); });
@@ -0,0 +1,64 @@
+import { type Client, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; +import { db, moderators } from "@/database"; +import config from "@/config"; + +const pronounTypes = { + subjective: "Subject (e.g., **They** are a moderator)", + objective: "Object (e.g., Send **them** a message)", + "possessive-determiner": "Possessive Determiner (e.g., This is **their** post)", + "possessive-pronoun": "Possessive Pronoun (e.g., The post is **theirs**)", + reflexive: "Reflexive (e.g., They assigned it to **themself**)" +} + +const command = new SlashCommandBuilder() + .setName("woke") + .setDescription("Set your pronouns."); + +Object.entries(pronounTypes).forEach(([key, description]) => { + command.addStringOption((option) => + option + .setName(key) + .setDescription(description) + .setRequired(true) + ); +}); + +command.addUserOption((option) => option.setName("user").setRequired(false).setDescription("The user to set pronouns for.")) + + +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction +) { + const id = interaction.options.getUser("user")?.id || interaction.user.id; + if (interaction.options.getUser("user") && interaction.user.id !== config.developerId) { + await interaction.reply({ content: "You cannot use this command.", ephemeral: true }); + return; + } + + const pronounData = { + subjective: interaction.options.getString("subjective", true), + objective: interaction.options.getString("objective", true), + possessiveDeterminer: interaction.options.getString("possessive-determiner", true), + possessive: interaction.options.getString("possessive-pronoun", true), + reflexive: interaction.options.getString("reflexive", true), + }; + + await db.insert(moderators) + .values({ + user_id: id, + ...pronounData, + }) + .onConflictDoUpdate({ + target: moderators.user_id, + set: pronounData, + }); + + await interaction.reply({ content: "Pronouns updated.", flags: ["Ephemeral"] }); +} + + +export default { + data: command, + execute, +};
@@ -21,7 +21,6 @@ type Client,
ContainerBuilder, DiscordAPIError, type GuildMember, - type Interaction, type ModalSubmitInteraction, PermissionFlagsBits, TextDisplayBuilder,@@ -29,6 +28,7 @@ type User,
} from "discord.js"; import { eq, sql } from "drizzle-orm"; import { getTicketContainer } from "./_shared"; +import { capitalise, getModerator } from "@/utils/woke"; const logger = loggers.interactions.child({ name: "ticket/actions" });@@ -145,6 +145,11 @@ targetUser ? targetUser.id : interaction.user.id,
); const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary) + .setEmoji("🙋♂️"), new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close")@@ -583,7 +588,11 @@ } catch (error) {
logger.error(error, `could not restore permissions for ${ticket.authorId}`); } - const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close")@@ -666,3 +675,110 @@ });
await interaction.editReply("✅ Transcript sent."); } + +export async function handleClaim( + _client: Client, + interaction: ButtonInteraction | ChatInputCommandInteraction, + ticket: Ticket, +) { + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); + if (!isStaff) { + await interaction.editReply({ + content: "Only staff can claim tickets.", + }); + return; + } + + if (ticket.claimedBy) { + await interaction.editReply({ + content: `This ticket is already claimed by <@${ticket.claimedBy}>.`, + }); + return; + } + + await db + .update(tickets) + .set({ + claimedBy: interaction.user.id, + claimedAt: new Date(), + }) + .where(eq(tickets.id, ticket.id)); + + const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:unclaim") + .setLabel("Unclaim") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId("ticket:close") + .setLabel("Close") + .setStyle(ButtonStyle.Danger) + .setEmoji("🔒"), + ); + + if (interaction.channel?.isSendable()) { + const moderator = getModerator(interaction.user.id) + const pronoun = capitalise(moderator.subjective) + + await interaction.channel.send({ + content: `This ticket has been claimed by <@${interaction.user.id}>! ${pronoun} will be with you shortly.`, + components: [controls], + }); + } + + await interaction.editReply("✅ Ticket claimed."); +} + +export async function handleUnclaim( + _client: Client, + interaction: ButtonInteraction | ChatInputCommandInteraction, + ticket: Ticket, +) { + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); + if (!isStaff) { + await interaction.editReply({ + content: "❌ Only staff can unclaim tickets.", + }); + return; + } + + if (!ticket.claimedBy) { + await interaction.editReply({ + content: "❌ This ticket is not claimed.", + }); + return; + } + + await db + .update(tickets) + .set({ + claimedBy: null, + claimedAt: null, + }) + .where(eq(tickets.id, ticket.id)); + + const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("ticket:close") + .setLabel("Close") + .setStyle(ButtonStyle.Danger) + .setEmoji("🔒"), + ); + + if (interaction.channel?.isSendable()) { + await interaction.channel.send({ + content: "This ticket is no longer claimed, someone else will be with you shortly.", + components: [controls], + }); + } + + await interaction.editReply("✅ Ticket unclaimed."); +}
@@ -14,12 +14,14 @@ import { parseComponentId } from "@/utils/discord/components";
import { getTicketReasonModal } from "./_shared"; import { handleAddUser, + handleClaim, handleClose, handleCreate, handleDelete, handleReopen, handleThank, handleTranscript, + handleUnclaim, } from "./actions"; import { handleExport,@@ -67,6 +69,18 @@ .setName("user")
.setDescription("The user to add") .setRequired(true), ), + ) + // claim + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("claim") + .setDescription("Claim the ticket in the current channel"), + ) + // unclaim + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("unclaim") + .setDescription("Unclaim the ticket in the current channel"), ) // close .addSubcommand(@@ -230,6 +244,9 @@ else if (action === "delete") await handleDelete(client, interaction, ticket);
else if (action === "transcript") await handleTranscript(client, interaction, ticket); else if (action === "add") await handleAddUser(client, interaction, ticket); + else if (action === "claim") await handleClaim(client, interaction, ticket); + else if (action === "unclaim") + await handleUnclaim(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); }@@ -279,6 +296,9 @@ if (action === "reopen") await handleReopen(client, interaction, ticket);
else if (action === "delete") await handleDelete(client, interaction, ticket); else if (action === "transcript") await handleTranscript(client, interaction, ticket); + else if (action === "claim") await handleClaim(client, interaction, ticket); + else if (action === "unclaim") + await handleUnclaim(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); }
@@ -1,13 +1,14 @@
import { db, ticketMessages, tickets } from "@/database"; import { getGuild } from "@/database/queries"; import { loggers } from "@/utils/logging"; -import type { Client } from "discord.js"; -import { and, eq } from "drizzle-orm"; +import { type Client } from "discord.js"; +import { and, desc, eq } from "drizzle-orm"; const logger = loggers.events.child({ name: "ticketWatcher" }); const WARN_MS = 15 * 60 * 1000; const CLOSE_MS = 30 * 60 * 1000; +const UNCLAIM_MS = 5 * 60 * 1000; const INTERVAL_MS = 60 * 1000; export function startTicketWatcher(client: Client) {@@ -24,7 +25,7 @@ "previous ticket watcher run still in progress, skipping this tick",
); return; } - running = true; + running = true; try { const openTickets = await db@@ -33,8 +34,62 @@ .from(tickets)
.where(eq(tickets.status, "open")) .execute(); - for (const ticket of openTickets) { + for (const ticket of openTickets) { try { + if (ticket.claimedBy && ticket.claimedAt) { + const claimedAt = new Date(ticket.claimedAt).getTime(); + if (Date.now() - claimedAt > UNCLAIM_MS) { + const [lastModMsg] = await db + .select() + .from(ticketMessages) + .where( + and( + eq(ticketMessages.ticketId, ticket.id), + eq(ticketMessages.authorType, "staff"), + eq(ticketMessages.authorId, ticket.claimedBy), + ), + ) + .orderBy(desc(ticketMessages.createdAt)) + .limit(1) + .execute(); + + const lastModActivity = lastModMsg + ? new Date(lastModMsg.createdAt).getTime() + : claimedAt; + + if (Date.now() - lastModActivity > UNCLAIM_MS) { + const [lastUserMsg] = await db + .select() + .from(ticketMessages) + .where( + and( + eq(ticketMessages.ticketId, ticket.id), + eq(ticketMessages.authorType, "user"), + ), + ) + .orderBy(desc(ticketMessages.createdAt)) + .limit(1) + .execute(); + + if ( + lastUserMsg && + new Date(lastUserMsg.createdAt).getTime() > lastModActivity + ) { + await db + .update(tickets) + .set({ + claimedBy: null, + claimedAt: null, + }) + .where(eq(tickets.id, ticket.id)) + .execute(); + + continue; + } + } + } + } + const createdAt = ticket.createdAt ? new Date(ticket.createdAt).getTime() : null;
@@ -0,0 +1,35 @@
+import { eq } from "drizzle-orm"; +import { db, moderators } from "@/database"; + +type Moderator = { + id: string; + /** they */ + subjective: string; + /** them */ + objective: string; + /** their */ + possessiveDeterminer: string; + /** theirs */ + possessive: string; + /** themself */ + reflexive: string; +} + + +const defaultModerator: Omit<Moderator, 'id'> = { + subjective: 'they', + objective: 'them', + possessiveDeterminer: 'their', + possessive: 'theirs', + reflexive: 'themself' +} + +export function getModerator(id: string): Moderator { + const moderator = db.select().from(moderators).where(eq(moderators.user_id, id)).get(); + if (!moderator) return { id, ...defaultModerator} + return { id: id, ...moderator }; +} + +export function capitalise(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +}