Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/features/tickets/id-allocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Ticket-Bot is licensed under the GNU Affero General Public License,
version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text.

Additional Term under GNU AGPL v3, Section 7(b):

You are required to preserve and display, in a location clearly visible
to end users interacting with the bot (such as bot embeds, the bot's
"Bio" Discord profile, status, or equivalent), a notice that the
software is powered by Ticket-Bot, including a link to the original
project repository or to its website.

This notice must not be removed, obscured, or replaced.
*/

import { sql } from "drizzle-orm";
import type { BotApp } from "@/core/types";
import { appMetaTable, ticketsTable } from "@/db/schema";

const TICKET_ID_SEQUENCE_KEY = "ticketIdSequence";

export async function reserveTicketId(app: Pick<BotApp, "db">) {
const updatedAt = Date.now();
const rows = await app.db
.insert(appMetaTable)
.values({
key: TICKET_ID_SEQUENCE_KEY,
value: sql<string>`CAST(COALESCE((SELECT MAX(${ticketsTable.id}) FROM ${ticketsTable}), 0) + 1 AS TEXT)`,
updatedAt
})
.onConflictDoUpdate({
target: appMetaTable.key,
set: {
// One upsert both catches up with existing rows and serializes concurrent reservations.
value: sql<string>`CAST(MAX(CAST(${appMetaTable.value} AS INTEGER), COALESCE((SELECT MAX(${ticketsTable.id}) FROM ${ticketsTable}), 0)) + 1 AS TEXT)`,
updatedAt
}
})
.returning({ value: appMetaTable.value });
const ticketId = Number(rows[0]?.value);

if (!Number.isSafeInteger(ticketId) || ticketId < 1) {
throw new Error("Failed to reserve a valid ticket ID.");
}

return ticketId;
}

/*
Ticket-Bot is licensed under the GNU Affero General Public License,
version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text.

Additional Term under GNU AGPL v3, Section 7(b):

You are required to preserve and display, in a location clearly visible
to end users interacting with the bot (such as bot embeds, the bot's
"Bio" Discord profile, status, or equivalent), a notice that the
software is powered by Ticket-Bot, including a link to the original
project repository or to its website.

This notice must not be removed, obscured, or replaced.
*/
11 changes: 5 additions & 6 deletions src/features/tickets/ticket-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
userCanAccessTicketType
} from "@/features/tickets/config-access";
import { TICKET_ACCESS_ALLOW } from "@/features/tickets/constants";
import { reserveTicketId } from "@/features/tickets/id-allocation";
import {
appendMessageText,
finalizeMessageTemplate,
Expand Down Expand Up @@ -280,7 +281,9 @@ async function createTicket(
}

const user = getInteractionUser(interaction);
const ticketNumber = (await getNextTicketNumber(app)).toString();
// Reserve before Discord work so every rendered number matches the eventual database primary key.
const ticketId = await reserveTicketId(app);
const ticketNumber = ticketId.toString();
const createdAt = Date.now();
const channelName = renderChannelName(ticketType.channelNameTemplate ?? app.config.tickets.channelNameTemplate, {
createdById: user.id,
Expand Down Expand Up @@ -318,6 +321,7 @@ async function createTicket(
await pinTicketWelcomeMessage(app, channel.id, ticketMessage.id);

await app.db.insert(ticketsTable).values({
id: ticketId,
channelId: channel.id,
creationMessageId: ticketMessage.id,
type: ticketTypeKey,
Expand Down Expand Up @@ -610,11 +614,6 @@ async function getUserOpenTicketCount(app: BotApp, userId: string) {
return Number(rows[0]?.count ?? 0);
}

async function getNextTicketNumber(app: BotApp) {
const rows = await app.db.select({ count: count() }).from(ticketsTable);
return Number(rows[0]?.count ?? 0) + 1;
}

function createDefaultTicketOpenReason(app: BotApp): TicketOpenReasonData {
return {
answers: [],
Expand Down
Loading