package public import ( "context" "fmt" "strconv" "strings" "unicode/utf8" "velox-bot/internal/db/repos/ticketrepo" "github.com/bwmarrin/discordgo" ) // The rest of the ticket configuration: the on/off switch, the panel's text, // the open ticket limit, editing a category, and looking at tickets. All of it // is available from Discord, so a dashboard is never needed. func ticketSettingsOptions() []*discordgo.ApplicationCommandOption { lo, hi := 1.0, 10.0 return []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "settings", Description: "Show the ticket settings, or change them. Leave out what you don't change", Options: []*discordgo.ApplicationCommandOption{ {Type: discordgo.ApplicationCommandOptionBoolean, Name: "enabled", Description: "Turn tickets on or off"}, {Type: discordgo.ApplicationCommandOptionString, Name: "panel-title", Description: "The panel's title", MaxLength: 256}, {Type: discordgo.ApplicationCommandOptionString, Name: "panel-description", Description: "The panel's text", MaxLength: 4000}, {Type: discordgo.ApplicationCommandOptionInteger, Name: "max-open", Description: "Open tickets one member can have, 1 to 10", MinValue: &lo, MaxValue: hi}, }, }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "list", Description: "List recent tickets", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "status", Description: "Open or closed (default: open)", Choices: []*discordgo.ApplicationCommandOptionChoice{{Name: "open", Value: ticketrepo.StatusOpen}, {Name: "closed", Value: ticketrepo.StatusClosed}}, }, }, }, } } func categoryEditOption() *discordgo.ApplicationCommandOption { return &discordgo.ApplicationCommandOption{ Type: discordgo.ApplicationCommandOptionSubCommand, Name: "edit", Description: "Change a category. Leave out what you don't change", Options: []*discordgo.ApplicationCommandOption{ {Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "The category to change", Required: true}, {Type: discordgo.ApplicationCommandOptionString, Name: "new-name", Description: "A new name", MaxLength: 40}, {Type: discordgo.ApplicationCommandOptionRole, Name: "staff-role", Description: "Who handles these tickets"}, {Type: discordgo.ApplicationCommandOptionBoolean, Name: "clear-staff-role", Description: "Remove the staff role"}, {Type: discordgo.ApplicationCommandOptionString, Name: "emoji", Description: "Shown on the button. Use - to remove", MaxLength: 64}, {Type: discordgo.ApplicationCommandOptionString, Name: "description", Description: "Shown on the panel. Use - to remove", MaxLength: 200}, }, } } func onOff(b bool) string { if b { return "on" } return "off" } func handleSettings(s *discordgo.Session, i *discordgo.InteractionCreate, o []*discordgo.ApplicationCommandInteractionDataOption) { ctx, cancel := context.WithTimeout(context.Background(), opTimeout) defer cancel() args := opts(o) g := guildID(i) if len(args) == 0 { st, err := Repo.GetSettings(ctx, g) if err != nil { reply(s, i, message("settings", err)) return } where := "not set (use `/tickets setup`)" if st.ChannelID != 0 { where = fmt.Sprintf("<#%d>", st.ChannelID) } logs := "none" if st.LogChannelID != 0 { logs = fmt.Sprintf("<#%d>", st.LogChannelID) } reply(s, i, fmt.Sprintf("**Tickets:** %s\n**Channel:** %s · **Log channel:** %s\n**Open tickets per member:** %d\n**Panel title:** %s\n**Panel text:** %s", onOff(st.Enabled), where, logs, st.MaxOpenPerUser, clipText(st.PanelTitle, 200), clipText(st.PanelDescription, 600))) return } var p ticketrepo.SettingsPatch if a, ok := args["enabled"]; ok { v := a.BoolValue() p.Enabled = &v } if a, ok := args["panel-title"]; ok { v := strings.TrimSpace(a.StringValue()) if v == "" || utf8.RuneCountInString(v) > 256 { reply(s, i, "The panel title must be between 1 and 256 characters.") return } p.PanelTitle = &v } if a, ok := args["panel-description"]; ok { v := strings.TrimSpace(a.StringValue()) if utf8.RuneCountInString(v) > 4000 { reply(s, i, "The panel text can be at most 4000 characters.") return } p.PanelDescription = &v } if a, ok := args["max-open"]; ok { v := int(a.IntValue()) if v < 1 || v > 10 { reply(s, i, "Open tickets per member must be between 1 and 10.") return } p.MaxOpenPerUser = &v } if err := Repo.UpdateSettings(ctx, g, p); err != nil { reply(s, i, message("settings", err)) return } msg := "Saved." if p.PanelTitle != nil || p.PanelDescription != nil { msg += " Run `/tickets panel` to update the panel with the new text." } if p.Enabled != nil { msg += " Tickets are now **" + onOff(*p.Enabled) + "**." } reply(s, i, msg) } func clipText(s string, n int) string { if r := []rune(s); len(r) > n { return string(r[:n-1]) + "…" } return s } func handleCategoryEdit(s *discordgo.Session, i *discordgo.InteractionCreate, o []*discordgo.ApplicationCommandInteractionDataOption) { ctx, cancel := context.WithTimeout(context.Background(), opTimeout) defer cancel() args := opts(o) g := guildID(i) name := strings.TrimSpace(args["name"].StringValue()) cats, err := Repo.ListCategories(ctx, g) if err != nil { reply(s, i, message("category edit", err)) return } var target *ticketrepo.Category for idx := range cats { if strings.EqualFold(cats[idx].Name, name) { target = &cats[idx] } } if target == nil { reply(s, i, fmt.Sprintf("There's no category called **%s**.", name)) return } var p ticketrepo.CategoryPatch if a, ok := args["new-name"]; ok { v := strings.TrimSpace(a.StringValue()) if v == "" || utf8.RuneCountInString(v) > 40 { reply(s, i, "The category name must be between 1 and 40 characters.") return } for _, c := range cats { if c.ID != target.ID && strings.EqualFold(c.Name, v) { reply(s, i, fmt.Sprintf("There's already a category called **%s**.", c.Name)) return } } p.Name = &v } if a, ok := args["staff-role"]; ok { id, _ := strconv.ParseInt(a.RoleValue(nil, i.GuildID).ID, 10, 64) p.StaffRoleID = &id } if a, ok := args["clear-staff-role"]; ok && a.BoolValue() { if p.StaffRoleID != nil { reply(s, i, "Choose either a staff role or clearing it, not both.") return } zero := int64(0) p.StaffRoleID = &zero } if a, ok := args["emoji"]; ok { v := strings.TrimSpace(a.StringValue()) if v == "-" { v = "" } p.Emoji = &v } if a, ok := args["description"]; ok { v := strings.TrimSpace(a.StringValue()) if v == "-" { v = "" } if utf8.RuneCountInString(v) > 200 { reply(s, i, "The category description can be at most 200 characters.") return } p.Description = &v } if p == (ticketrepo.CategoryPatch{}) { reply(s, i, "Say what to change: new-name, staff-role, clear-staff-role, emoji or description.") return } if _, err := Repo.UpdateCategory(ctx, g, target.ID, p); err != nil { reply(s, i, message("category edit", err)) return } reply(s, i, fmt.Sprintf("Updated **%s**. Run `/tickets panel` to update the panel.", target.Name)) } func handleList(s *discordgo.Session, i *discordgo.InteractionCreate, o []*discordgo.ApplicationCommandInteractionDataOption) { ctx, cancel := context.WithTimeout(context.Background(), opTimeout) defer cancel() status := ticketrepo.StatusOpen if a, ok := opts(o)["status"]; ok { status = a.StringValue() } ts, err := Repo.ListTickets(ctx, guildID(i), status, 15) if err != nil { reply(s, i, message("list", err)) return } if len(ts) == 0 { reply(s, i, "No "+status+" tickets.") return } var b strings.Builder fmt.Fprintf(&b, "**Latest %s tickets**\n", status) for _, t := range ts { fmt.Fprintf(&b, "• #%d <#%d> %s, by <@%d>", t.Number, t.ThreadID, clipText(t.CategoryName, 30), t.OpenerID) if t.ClaimedBy != 0 { fmt.Fprintf(&b, ", claimed by <@%d>", t.ClaimedBy) } b.WriteString("\n") } reply(s, i, clipText(b.String(), 1900)) }