Tickets: settings, category edit and ticket list from Discord; NOTIFY triggers in schema.sql

- /tickets settings (on or off, panel title and text, open tickets per member), /tickets category edit, /tickets list
- schema.sql now creates the NOTIFY triggers, so the integration tests run on a database built from it, and two stale expectations (tickets start off) are fixed

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
FernandoJVideiraandClaude Sonnet 5.5 committed 2026-10-23 02:05:45 +01:00
1 parent d95ebf473e
commit acdb5ead35
6 files changed
+483 -20

No files matched your search

+109
View File
@@ -7,6 +7,8 @@ import (
"context"
"database/sql"
"errors"
"strconv"
"strings"
"time"
)
@@ -301,3 +303,110 @@ func (r *Repo) MarkCommandProcessed(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, `UPDATE ticket_commands SET processed_at = NOW() WHERE id = $1`, id)
return err
}
// SettingsPatch changes ticket settings. A nil field is left as it is.
type SettingsPatch struct {
Enabled *bool
PanelTitle *string
PanelDescription *string
MaxOpenPerUser *int
}
// UpdateSettings applies the fields that are set. The server gets a settings
// row if it has none yet, with everything else at its default and the system
// still off, until /tickets setup or this turns it on.
func (r *Repo) UpdateSettings(ctx context.Context, guildID int64, p SettingsPatch) error {
if _, err := r.db.ExecContext(ctx, `INSERT INTO ticket_settings (guild_id) VALUES ($1) ON CONFLICT DO NOTHING`, guildID); err != nil {
return err
}
var sets []string
args := []any{guildID}
add := func(col string, v any) {
args = append(args, v)
sets = append(sets, col+` = $`+strconv.Itoa(len(args)))
}
if p.Enabled != nil {
add("enabled", *p.Enabled)
}
if p.PanelTitle != nil {
add("panel_title", *p.PanelTitle)
}
if p.PanelDescription != nil {
add("panel_description", *p.PanelDescription)
}
if p.MaxOpenPerUser != nil {
add("max_open_per_user", *p.MaxOpenPerUser)
}
if len(sets) == 0 {
return nil
}
_, err := r.db.ExecContext(ctx, `UPDATE ticket_settings SET `+strings.Join(sets, ", ")+` WHERE guild_id = $1`, args...)
return err
}
// CategoryPatch changes a category. A nil field is left as it is; an empty
// emoji or description clears it, and a zero StaffRoleID clears the role.
type CategoryPatch struct {
Name *string
Emoji *string
Description *string
StaffRoleID *int64
}
// UpdateCategory applies the fields that are set and returns the category as
// saved, or nil when there is none with that ID in the guild.
func (r *Repo) UpdateCategory(ctx context.Context, guildID, id int64, p CategoryPatch) (*Category, error) {
var sets []string
args := []any{guildID, id}
add := func(col string, v any) {
args = append(args, v)
sets = append(sets, col+` = $`+strconv.Itoa(len(args)))
}
if p.Name != nil {
add("name", *p.Name)
}
if p.Emoji != nil {
add("emoji", nullText(*p.Emoji))
}
if p.Description != nil {
add("description", *p.Description)
}
if p.StaffRoleID != nil {
add("staff_role_id", nullID(*p.StaffRoleID))
}
if len(sets) == 0 {
return r.GetCategory(ctx, guildID, id)
}
q := `UPDATE ticket_categories SET ` + strings.Join(sets, ", ") + ` WHERE guild_id = $1 AND id = $2 RETURNING ` + categoryCols
c, err := scanCategory(r.db.QueryRowContext(ctx, q, args...))
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &c, nil
}
// ListTickets returns a guild's most recent tickets with the given status
// ("open" or "closed"), newest first.
func (r *Repo) ListTickets(ctx context.Context, guildID int64, status string, limit int) ([]Ticket, error) {
if limit < 1 || limit > 100 {
limit = 25
}
q := `SELECT ` + ticketCols + ` FROM tickets WHERE guild_id = $1 AND status = $2 ORDER BY number DESC LIMIT $3`
rows, err := r.db.QueryContext(ctx, q, guildID, status, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Ticket
for rows.Next() {
t, err := scanTicket(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}