package tickets import ( "context" "fmt" "regexp" "strconv" "strings" "velox-bot/internal/db/repos/ticketrepo" "github.com/bwmarrin/discordgo" ) // Custom IDs. Everything starts with "tickets:" so the interaction router can // hand it to this package. const ( idOpenPrefix = "tickets:open:" // older panels: one button per category, + category ID idSelect = "tickets:select" // panel dropdown idCreate = "tickets:create" // panel "Create ticket" button idOpenFormPrefix = "tickets:openform:" // opening form, + category ID idClaim = "tickets:claim" idClose = "tickets:close" idCloseForm = "tickets:closeform" fieldSubject = "subject" fieldDetails = "details" fieldReason = "reason" ) var customEmojiRe = regexp.MustCompile(`^<(a?):(\w+):(\d+)>$`) // parseEmoji accepts a Unicode emoji or a custom one written as <:name:id>. func parseEmoji(s string) *discordgo.ComponentEmoji { s = strings.TrimSpace(s) if s == "" { return nil } if m := customEmojiRe.FindStringSubmatch(s); m != nil { return &discordgo.ComponentEmoji{Name: m[2], ID: m[3], Animated: m[1] == "a"} } return &discordgo.ComponentEmoji{Name: s} } // BuildPanel is the message members open tickets from: an embed with the // guild's text and each category's description, a dropdown to pick the // category, and a Create ticket button that opens the form for that pick. func BuildPanel(s *ticketrepo.Settings, cats []ticketrepo.Category) (*discordgo.MessageEmbed, []discordgo.MessageComponent) { embed := &discordgo.MessageEmbed{ Title: clip(s.PanelTitle, 256), Description: clip(s.PanelDescription, 4000), Color: accent, } if len(cats) > maxCategories { cats = cats[:maxCategories] } opts := make([]discordgo.SelectMenuOption, 0, len(cats)) for _, c := range cats { opts = append(opts, discordgo.SelectMenuOption{ Label: clip(c.Name, 100), Value: id(c.ID), Description: clip(c.Description, 100), Emoji: parseEmoji(c.Emoji), }) if c.Description != "" { embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{Name: clip(c.Name, 256), Value: clip(c.Description, 1024)}) } } return embed, []discordgo.MessageComponent{ discordgo.ActionsRow{Components: []discordgo.MessageComponent{ discordgo.SelectMenu{CustomID: idSelect, Placeholder: "Choose what you need help with", Options: opts}, }}, discordgo.ActionsRow{Components: []discordgo.MessageComponent{ discordgo.Button{Label: "Create ticket", Style: discordgo.PrimaryButton, CustomID: idCreate, Emoji: &discordgo.ComponentEmoji{Name: "🎫"}}, }}, } } // PostPanel posts the panel in the ticket channel, or updates the one it // posted before, and returns where it ended up. func PostPanel(ctx context.Context, d Discord, st Store, guildID int64) (*discordgo.Message, error) { s, err := st.GetSettings(ctx, guildID) if err != nil { return nil, err } if s.ChannelID == 0 { return nil, ErrNotSetUp } cats, err := st.ListCategories(ctx, guildID) if err != nil { return nil, err } if len(cats) == 0 { return nil, ErrNoCategories } embed, comps := BuildPanel(s, cats) if s.PanelMessageID != 0 { embeds := []*discordgo.MessageEmbed{embed} msg, err := d.ChannelMessageEditComplex(&discordgo.MessageEdit{ Channel: id(s.ChannelID), ID: id(s.PanelMessageID), Embeds: &embeds, Components: &comps, }) if err == nil { return msg, nil } // The old panel was deleted or the channel changed: post a new one. } msg, err := d.ChannelMessageSendComplex(id(s.ChannelID), &discordgo.MessageSend{ Embeds: []*discordgo.MessageEmbed{embed}, Components: comps, }) if err != nil { return nil, fmt.Errorf("posting the panel: %w", err) } if mid, err := strconv.ParseInt(msg.ID, 10, 64); err == nil { if err := st.SetPanelMessage(ctx, guildID, mid); err != nil { return msg, fmt.Errorf("saving the panel message: %w", err) } } return msg, nil } // OpenForm is the short form shown after a member picks a category. func OpenForm(cat *ticketrepo.Category) *discordgo.InteractionResponse { return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseModal, Data: &discordgo.InteractionResponseData{ CustomID: idOpenFormPrefix + id(cat.ID), Title: clip("Open a ticket: "+cat.Name, 45), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{Components: []discordgo.MessageComponent{discordgo.TextInput{ CustomID: fieldSubject, Label: "What do you need help with?", Style: discordgo.TextInputShort, Placeholder: "A short summary", Required: true, MinLength: 3, MaxLength: maxSubject, }}}, discordgo.ActionsRow{Components: []discordgo.MessageComponent{discordgo.TextInput{ CustomID: fieldDetails, Label: "Details", Style: discordgo.TextInputParagraph, Placeholder: "Anything the staff should know (optional)", Required: false, MaxLength: maxDetails, }}}, }, }, } } // CloseForm asks for an optional reason before closing. func CloseForm() *discordgo.InteractionResponse { return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseModal, Data: &discordgo.InteractionResponseData{ CustomID: idCloseForm, Title: "Close this ticket", Components: []discordgo.MessageComponent{ discordgo.ActionsRow{Components: []discordgo.MessageComponent{discordgo.TextInput{ CustomID: fieldReason, Label: "Reason (optional)", Style: discordgo.TextInputParagraph, Required: false, MaxLength: maxReason, }}}, }, }, } }