Skip to main content

Tickets concept

This page explains how the ticket system works across the player shop, the admin queue, and Discord.

Use it when you need the shared mechanics behind ticket visibility, status changes, participants, and Discord behavior.

What a ticket containsโ€‹

Each ticket combines:

  • a subject
  • a status
  • a participant list
  • a message history
  • an optional assignee
  • an optional Discord channel
  • an optional request type ("category") and the answers the player gave when opening it

Shop-created tickets automatically add the creator as a participant. Staff-created tickets can start without participants and be filled later from the admin queue.

Staff-created and AI-created tickets have no request type: they never run an intake form, so there is nothing to record.

Every ticket is shown by a four-character code such as #7KQM โ€” in the app, in Discord and in the AI companion's answers. Codes are random, so they reveal nothing about how many tickets exist or in which order they were opened. The ticket search finds a ticket by its code, and the companion accepts a code wherever it asks for a ticket.

Ticket lifecycleโ€‹

Tickets move through these states:

  • Open: newly created or reopened
  • In progress: the first public reply turns an open ticket into in progress
  • Closed: the ticket is resolved but can still be reopened
  • Archived: the ticket is finished and the Discord channel is removed

Important behavior:

  • reopening a closed ticket keeps the existing Discord channel and unlocks it again
  • reopening an archived ticket creates a fresh Discord channel and replays the recent public history
  • if Tickets Auto Archive is enabled, closing a ticket archives it immediately instead of leaving it in the closed state
  • a ticket, with its messages and attachments, is deleted 12 months after its last activity

Participants and visibilityโ€‹

Participants define which players can see a ticket in Shop -> Tickets.

Staff can add:

  • single players
  • all current members of a faction

Important behavior:

  • only participants can see a ticket in the shop
  • internal notes are never shown to players
  • adding or removing a participant also updates Discord channel access for linked Discord accounts

Discord behaviorโ€‹

Navigation path for setup:

  • Server Admin -> Settings -> Discord

Relevant settings:

  • Discord Server
  • Discord Tickets Channel Prefix
  • Tickets Auto Archive

When Discord is configured and the bot is installed:

  • each ticket gets its own Discord text channel, created inside its support level's category (set under Server Admin โ†’ Tickets โ†’ Levels); if that category was deleted in Discord, the channel is created outside any category until you pick a new one
  • each support level owns a bot-managed Discord role whose members can read that level's channels and the channels of every level below
  • staff receive the role of their highest level automatically, driven by their level rights in dzbot
  • public messages are mirrored between dzbot and Discord
  • internal notes stay private in dzbot and only post a short notice into the Discord channel
  • archived tickets send participants a DM with reopen and view actions

Attention stateโ€‹

Each active ticket tracks who sent the last public message. This drives in-app badges shown on every ticket row in the admin queue and the player shop list:

  • Needs Attention โ€” the last public message is from a player; support has not yet replied
  • Waiting for Response โ€” the last public message is from support; the player has not yet replied

Discord channel header actionsโ€‹

The pinned header message carries a single row of buttons. Discord allows five per row, which is why everything else moved into a More โ€ฆ menu.

On an open ticket:

  • Reply as Support โ€” opens the reply modal; the answer is posted anonymously by the bot
  • Close Ticket
  • Assign to me, or Remove assignee once somebody is assigned
  • Remind Participants โ€” sends a response request by DM
  • More โ€ฆ

Once the ticket is closed, the first two are replaced by Reopen Ticket and Archive Ticket; More โ€ฆ stays.

The "More โ€ฆ" menuโ€‹

Pressing it answers privately โ€” only the person who pressed it sees the menu, and it names the current assignee, which the public header deliberately does not. It contains:

  • Mark: Waiting for Support / Mark: Waiting for Player โ€” flips the responsibility badge
  • Change support level โ€” a select of the other levels; picking one asks for a reason. Only on servers with more than one level, and only shown to someone holding admin.tickets.change_level for this ticket's level โ€” the level names never reach the ticket's participants.
  • Assign ticket toโ€ฆ โ€” a user-select for assigning to somebody other than yourself
  • Add participantโ€ฆ โ€” a user-select, up to 10 at a time
  • Add participants by roleโ€ฆ โ€” a role-select, up to 5 at a time

Each entry checks its own access right when used: admin.tickets.assign for the assignment controls, admin.tickets.add_participant for both participant selects, admin.tickets.change_level for the level change, admin.tickets.reply for the waiting toggle.

When a selected user has no player account on this server, the bot answers privately that they cannot be assigned. Re-selecting an existing participant is silently ignored.

Answering without leaving Discordโ€‹

Two slash commands work inside a ticket channel:

  • /answer โ€” replies to the ticket as support, posted anonymously by the bot. Needs admin.tickets.reply.
  • /note โ€” adds an internal note that only supporters can read. Needs admin.tickets.internal_message.

Both take an optional text argument. Leave it empty and the bot opens a modal instead, which also offers a file upload field โ€” up to five attachments. The commands themselves deliberately carry no attachment option; attachments go through the modal.

Request types and the intake formโ€‹

Every server has a configurable set of request types (categories). Each type owns a small form the player fills in before the ticket exists, so a report arrives complete instead of as "hi I have a problem": a faction application asks for the faction name, a multi-account report asks who is being reported and what the evidence is.

The point is a clean intake, not analytics. Nothing in the system reads the structured answers back โ€” they exist so that staff can see exactly what was asked and exactly what came back.

Field typesโ€‹

A form has at most five fields. That is not a design preference: a Discord modal takes "between 1 and 5 components" and a modal submit cannot open a second modal, and both surfaces have to show the identical form โ€” so Discord is the floor.

TypeDiscordShop
Short textsingle-line text input<input>
Long textmulti-line text input<textarea>
Choicedropdown, radio group or checkbox groupselect, radios or checkboxes
Checkboxcheckbox (a required one must be ticked)checkbox
File uploadfile upload, 1โ€“10 filesfile input

A field also carries an optional hint, which is shown next to the input and costs no form slot.

How the answers become a ticketโ€‹

Each type has two templates, both localized:

  • the subject template, which produces the ticket subject
  • the message template, which produces the ticket's first message

Available placeholders:

PlaceholderSubjectMessageValue
{category}โœ…โœ…the request type's name in the language the form was filled in
{player}โœ…โœ…the creator's display name
{field:<key>}โœ…โœ…one field's answer (choice โ†’ the option labels, checkbox โ†’ yes/no, file โ†’ the filenames)
{fields}โŒโœ…one Label: value line per filled field, in form order

Rules that matter in practice:

  • a placeholder that resolves to nothing is replaced with nothing, and a line that ends up blank is dropped
  • the subject is collapsed to one line and trimmed to 100 characters; if it renders empty it falls back to the request type's name, and then to "Ticket"
  • the first message has the same chain, falling back to the request type's name and then to a dash โ€” a form whose fields are all optional and all left blank must not produce a blank opening post
  • templates are plain text; ticket messages are mirrored into Discord and rendered in the shop, and markdown would show its asterisks on one of the two

The answer snapshotโ€‹

Alongside the rendered message, the ticket stores a snapshot of the intake: the field labels and the selected option labels are copied into it. A ticket is a record, so renaming a field or deleting the request type three months later never rewrites or blanks an old ticket. Deleting a type leaves its tickets readable; they only lose their badge and their place in the queue filter.

The welcome messageโ€‹

A request type can carry a welcome message, posted as the opening staff message before the player's own first message, so it reads as a preamble. It is localized like everything else on the type and is resolved in the language the form was filled in.

This used to be one server-wide setting (Ticket Welcome Message). It moved onto the request type because what a faction applicant needs to be told is not what a multi-account reporter needs to be told. Two consequences:

  • the server setting no longer exists; each type is configured on its own, and an empty one posts nothing
  • a ticket without a request type gets no welcome message at all โ€” that means staff-created tickets (which never had one), AI-created tickets, and the legacy ticket_embed_create button until its server publishes real panels

Like the intake message it is plain text, and it is only posted when the ticket has a Discord channel.

Discord panelsโ€‹

A request type can own a panel message per Discord channel: an embed with the type's name and description, and one "open ticket" button per maintained language. The clicked button carries the language, so nothing has to be guessed from the player's Discord settings.

Several panels per type is the normal case, not an edge one: a guild that separates registered players from guests has more than one channel a ticket may be opened from, and the same type has to be reachable from each. The button carries only the type and the language, so every panel of a type is interchangeable โ€” a ticket opened from the guest channel is the same ticket as one opened from the members channel.

A language counts as maintained only when the type's name and every field label are filled in it โ€” otherwise a German player would face a half-English form.

The panel speaks the server's language: the embed title, the description leading it and the first button all use the language configured for the server. Every other maintained language follows behind it โ€” its description as a section headed in that language ("English version" on a German server), its button next in the row. A type maintained in a single language renders that one unlabelled, whichever it is.

Panels are managed: each one's channel and message id are stored and the message is edited in place whenever the type changes, because a stale panel whose button no longer matches its form is the classic ticket-bot failure. Three consequences:

  • publishing is refused until the type has at least one field and at least one maintained language
  • every edit fans out over all of the type's panels, and one channel where the bot lost permissions does not stop the others from being rewritten
  • Discord cannot reorder messages, so reordering request types does not move their panels; there is an explicit "Rebuild panels" action that deletes and reposts the panels of one channel

A panel row lives exactly as long as its Discord message: if someone deletes the message, the next edit attempt drops the row and the editor offers to publish it again.

The /ticket commandโ€‹

Every member can type /ticket anywhere in the guild โ€” players coming from other ticket bots expect it. It offers the same request types a panel would and opens the same form, so the resulting ticket is identical to one opened from a panel:

  • exactly one active request type with at least one field โ†’ its form opens straight away
  • several โ†’ a private selection list with each type's name and the start of its description; picking one opens its form
  • none, or a guild not connected to a server โ†’ a private explanation

The form uses the player's Discord language when the type is maintained in it, otherwise the server's language. On a guild several servers share, the server is taken from the channel's category; failing that, from the one server the player is linked on; failing that, from the server that owns no categories. If none of these decides it, the player is asked to use the command in a channel of their server.

Opening a ticket from Discord โ€” with /ticket, a request-type panel or the legacy ticket panel โ€” needs the player right player.tickets.create, exactly like the web. A Discord member who has not linked an account yet is judged by the server's default groups; banned players keep their ticket rights. All other slash commands are described in Discord commands.

Routingโ€‹

A request type can name a default support level, so the most common routing wish โ€” "this kind of request always belongs to team X" โ€” needs no manual escalation. Without one, tickets of that type start in the entry queue like everything else.

Support levelsโ€‹

Ticket handling can be split into support levels, so first-level staff work the entry queue only and escalated tickets become invisible to them.

  • Every server has at least one level. New tickets always enter at position 1.
  • Levels are configurable in count and name under Server Admin -> Tickets -> Levels.
  • Visibility is cumulative: the highest level a staff member holds wins, and every level below it stays visible. Forgetting a checkbox costs sight of the level above, never of the tickets below.
  • A ticket's level is authoritative in dzbot. Everything staff-facing respects it: the queue, the ticket detail page, global search, the AI companion's ticket tools, the player dossier, and the Discord channel buttons.
  • Players never see levels. A participant always reaches their own ticket, wherever staff moved it to.
  • Dashboards and statistics are deliberately not level-filtered โ€” those are system-wide numbers.

Escalating and de-escalatingโ€‹

A ticket can be moved to any higher level, and back down to any lower one, from the admin ticket detail page or from the pinned Discord channel header. Both require admin.tickets.change_level plus current sight of the ticket, and both demand a reason.

On every level change:

  • the reason is recorded as an internal note naming both levels and the acting staff member
  • the assignee is cleared, so the ticket surfaces as unhandled in the receiving queue
  • status and attention state stay untouched
  • the Discord channel moves into the new level's category and its role overwrites are swapped

Moving a ticket above your own level is allowed; the app warns you first and returns you to the queue afterwards, because the ticket is no longer yours to see.

Levels in Discordโ€‹

Each level owns:

  • a category, created by the bot when the level is created and swappable later from the management page
  • a bot-managed role whose membership follows the level right in dzbot โ€” dzbot rights are the single source of truth, exactly as for the Connected and Online roles

Visibility is cumulative, but a staff member still carries only one level role: the one of their highest level. Each channel admits the role of its own level and of every level above, so someone on level 3 reads the level 1 and 2 channels with the level 3 role alone. Discord administrators see every channel anyway and get no level role at all.

If Discord is not installed or the bot lacks permissions, levels still work โ€” the category and role stay empty and the management drawer offers a repair action.

Locked ticket referencesโ€‹

A note, restriction or dossier entry may point at a ticket the viewer cannot see. Such a reference keeps its link and is marked with a lock, but its subject is withheld. Opening it shows which level the ticket sits in instead of a generic access error, so the surrounding context is never silently lost.

Access-right modelโ€‹

Player-facing rights:

  • player.tickets.view to open the shop ticket area
  • player.tickets.create to create new tickets
  • player.tickets.send_message to reply and edit the subject while the ticket is active
  • player.tickets.close to close active tickets
  • player.tickets.archive to archive resolved tickets when auto-archive is not active

Admin-facing rights:

  • admin.tickets.view for read-only access to the admin queue
  • admin.tickets.create for staff-created tickets
  • admin.tickets.reply for public support replies, message pinning, and evidence file uploads
  • admin.tickets.internal_message for internal notes
  • admin.tickets.assign for assignee, participants, and subject management in the admin panel
  • admin.tickets.change_status for manual status changes
  • admin.tickets.add_participant to add participants via the Discord channel user-select
  • admin.tickets.change_level to move a ticket between support levels
  • admin.tickets.manage_levels to create, rename, reorder and delete support levels
  • admin.tickets.manage_categories to create, edit, reorder and delete request types and publish their Discord panels
  • admin.tickets.level_<id> โ€” one right per support level; see Support levels
  • logs.view to use the Investigation tab on the ticket detail page
  • restrictions.view to view linked restrictions on the ticket Review tab
  • restrictions.create to create restrictions directly from a ticket

For group strategy and mixed staff/player setups, see Access control.