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
51 changes: 51 additions & 0 deletions bot/button/handlers/closewithreason_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package handlers

import (
"encoding/json"
"testing"

"github.com/TicketsBot-cloud/database"
"github.com/stretchr/testify/require"
)

func TestPickCloseReason(t *testing.T) {
require.Equal(t, "Resolved", pickCloseReason("typed text", []string{"Resolved"}))
require.Equal(t, "typed text", pickCloseReason(" typed text ", nil))
require.Equal(t, "", pickCloseReason(" ", []string{}))
}

func TestBuildCloseReasonSelectMenu(t *testing.T) {
current := "resolved"

tests := []struct {
name string
closeReasons database.PanelCloseReasons
current *string
want string
}{
{
name: "custom allowed",
closeReasons: database.PanelCloseReasons{Reasons: []string{"Resolved", "Duplicate"}, AllowCustom: true},
want: `{"type":3,"custom_id":"preset","options":[{"label":"Resolved","value":"Resolved","default":false},{"label":"Duplicate","value":"Duplicate","default":false}],"min_values":0,"max_values":1,"disabled":false,"required":false}`,
},
{
name: "custom not allowed",
closeReasons: database.PanelCloseReasons{Reasons: []string{"Resolved"}, AllowCustom: false},
want: `{"type":3,"custom_id":"preset","options":[{"label":"Resolved","value":"Resolved","default":false}],"max_values":1,"disabled":false,"required":true}`,
},
{
name: "current reason selected",
closeReasons: database.PanelCloseReasons{Reasons: []string{"Resolved", "Duplicate"}, AllowCustom: true},
current: &current,
want: `{"type":3,"custom_id":"preset","options":[{"label":"Resolved","value":"Resolved","default":true},{"label":"Duplicate","value":"Duplicate","default":false}],"min_values":0,"max_values":1,"disabled":false,"required":false}`,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
marshalled, err := json.Marshal(buildCloseReasonSelectMenu(tc.closeReasons, tc.current))
require.NoError(t, err)
require.JSONEq(t, tc.want, string(marshalled))
})
}
}
68 changes: 68 additions & 0 deletions bot/button/handlers/closewithreasonmodal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package handlers
import (
"time"

"github.com/TicketsBot-cloud/common/sentry"
"github.com/TicketsBot-cloud/database"
"github.com/TicketsBot-cloud/gdl/objects/interaction"
"github.com/TicketsBot-cloud/gdl/objects/interaction/component"
"github.com/TicketsBot-cloud/worker/bot/button"
Expand All @@ -11,6 +13,7 @@ import (
"github.com/TicketsBot-cloud/worker/bot/command/context"
"github.com/TicketsBot-cloud/worker/bot/customisation"
"github.com/TicketsBot-cloud/worker/bot/dbclient"
"github.com/TicketsBot-cloud/worker/bot/logic"
"github.com/TicketsBot-cloud/worker/bot/utils"
"github.com/TicketsBot-cloud/worker/i18n"
)
Expand Down Expand Up @@ -47,6 +50,20 @@ func (h *CloseWithReasonModalHandler) Execute(ctx *context.ButtonContext) {
return
}

closeReasons, err := logic.GetPanelCloseReasons(ctx, ticket)
if err != nil {
sentry.ErrorWithContext(err, ctx.ToErrorContext())
} else if len(closeReasons.Reasons) > 0 {
ctx.Modal(button.ResponseModal{
Data: interaction.ModalResponseData{
CustomId: "close_with_reason_submit",
Title: i18n.TitleClose.GetFromGuild(ctx.GuildId()),
Components: buildCloseReasonPresetComponents(ctx.GuildId(), closeReasons),
},
})
return
}

ctx.Modal(button.ResponseModal{
Data: interaction.ModalResponseData{
CustomId: "close_with_reason_submit",
Expand All @@ -67,3 +84,54 @@ func (h *CloseWithReasonModalHandler) Execute(ctx *context.ButtonContext) {
},
})
}

func buildCloseReasonPresetComponents(guildId uint64, closeReasons database.PanelCloseReasons) []component.Component {
components := []component.Component{
component.BuildLabel(component.Label{
Label: i18n.Reason.GetFromGuild(guildId),
Component: buildCloseReasonSelectMenu(closeReasons, nil),
}),
}

if !closeReasons.AllowCustom {
return components
}

return append(components, component.BuildLabel(component.Label{
Label: i18n.MessageCloseReasonCustom.GetFromGuild(guildId),
Description: utils.Ptr(i18n.MessageCloseReasonCustomDescription.GetFromGuild(guildId)),
Component: component.BuildInputText(component.InputText{
Style: component.TextStyleParagraph,
CustomId: "reason",
Placeholder: utils.Ptr(i18n.MessageCloseReasonPlaceholder.GetFromGuild(guildId)),
MaxLength: utils.Ptr(uint32(1024)),
Required: utils.Ptr(false),
}),
}))
}

func buildCloseReasonSelectMenu(closeReasons database.PanelCloseReasons, current *string) component.Component {
selected, hasSelected := closeReasons.Match(utils.ValueOrZero(current))

options := make([]component.SelectOption, len(closeReasons.Reasons))
for i, reason := range closeReasons.Reasons {
options[i] = component.SelectOption{
Label: reason,
Value: reason,
Default: hasSelected && reason == selected,
}
}

selectMenu := component.SelectMenu{
CustomId: "preset",
Options: options,
MaxValues: utils.Ptr(1),
Required: utils.Ptr(!closeReasons.AllowCustom),
}

if closeReasons.AllowCustom {
selectMenu.MinValues = utils.Ptr(0)
}

return component.BuildSelectMenu(selectMenu)
}
49 changes: 26 additions & 23 deletions bot/button/handlers/closewithreasonsubmit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package handlers

import (
"fmt"
"strings"

"github.com/TicketsBot-cloud/gdl/objects/interaction"
"github.com/TicketsBot-cloud/worker/bot/button/registry"
"github.com/TicketsBot-cloud/worker/bot/button/registry/matcher"
"github.com/TicketsBot-cloud/worker/bot/command/context"
"github.com/TicketsBot-cloud/worker/bot/constants"
"github.com/TicketsBot-cloud/worker/bot/customisation"
"github.com/TicketsBot-cloud/worker/bot/logic"
"github.com/TicketsBot-cloud/worker/i18n"
)

type CloseWithReasonSubmitHandler struct{}
Expand All @@ -25,39 +27,40 @@ func (h *CloseWithReasonSubmitHandler) Properties() registry.Properties {
}

func (h *CloseWithReasonSubmitHandler) Execute(ctx *context.ModalContext) {
data := ctx.Interaction.Data

// Get the reason
if len(data.Components) == 0 { // No action rows
ctx.HandleError(fmt.Errorf("No action rows found in modal components"))
return
}

actionRow := data.Components[0]
if len(actionRow.Components) == 0 && actionRow.Component == nil { // Text input missing
reason, hasInput := ctx.GetInput("reason")
presets, hasSelect := ctx.GetValues("preset")
if !hasInput && !hasSelect {
ctx.HandleError(fmt.Errorf("Modal missing text input"))
return
}

var textInput interaction.ModalSubmitInteractionComponentData

if actionRow.Component != nil {
textInput = *actionRow.Component
} else {
textInput = actionRow.Components[0]
if hasSelect {
reason = pickCloseReason(reason, presets)
if reason == "" {
ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseReasonMissing)
return
}
}

if textInput.CustomId != "reason" {
ctx.HandleError(fmt.Errorf("Text input custom ID mismatch"))
// This must be malicious
if len(reason) > 1024 {
ctx.HandleError(fmt.Errorf("Reason is too long"))
return
}

// This must be malicious
if len(textInput.Value) > 1024 {
ctx.HandleError(fmt.Errorf("Reason is too long"))
reason, ok := logic.ResolveChannelCloseReason(ctx.Context, ctx, reason)
if !ok {
return
}

ctx.Ack()
logic.CloseTicket(ctx.Context, ctx, &textInput.Value, false)
logic.CloseTicket(ctx.Context, ctx, &reason, false)
}

func pickCloseReason(text string, presets []string) string {
if len(presets) > 0 {
return presets[0]
}

return strings.TrimSpace(text)
}
48 changes: 48 additions & 0 deletions bot/button/handlers/editclosereasonmodal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"

"github.com/TicketsBot-cloud/common/permission"
"github.com/TicketsBot-cloud/common/sentry"
"github.com/TicketsBot-cloud/database"
"github.com/TicketsBot-cloud/gdl/objects/interaction"
"github.com/TicketsBot-cloud/gdl/objects/interaction/component"
"github.com/TicketsBot-cloud/worker/bot/button"
Expand Down Expand Up @@ -73,6 +75,20 @@ func (h *EditCloseReasonModalHandler) Execute(ctx *context.ButtonContext) {
return
}

closeReasons, err := dbclient.Client.PanelCloseReasons.GetByTicket(ctx.Context, guildId, ticketId)
if err != nil {
sentry.ErrorWithContext(err, ctx.ToErrorContext())
} else if len(closeReasons.Reasons) > 0 {
ctx.Modal(button.ResponseModal{
Data: interaction.ModalResponseData{
CustomId: fmt.Sprintf("edit_close_reason_submit_%d_%d", guildId, ticketId),
Title: "Edit Close Reason",
Components: buildEditCloseReasonPresetComponents(closeMetadata.Reason, closeReasons),
},
})
return
}

ctx.Modal(button.ResponseModal{
Data: interaction.ModalResponseData{
CustomId: fmt.Sprintf("edit_close_reason_submit_%d_%d", guildId, ticketId),
Expand All @@ -93,3 +109,35 @@ func (h *EditCloseReasonModalHandler) Execute(ctx *context.ButtonContext) {
},
})
}

func buildEditCloseReasonPresetComponents(current *string, closeReasons database.PanelCloseReasons) []component.Component {
components := []component.Component{
component.BuildLabel(component.Label{
Label: "Close Reason",
Description: utils.Ptr("Update the reason this ticket was closed"),
Component: buildCloseReasonSelectMenu(closeReasons, current),
}),
}

if !closeReasons.AllowCustom {
return components
}

value := current
if _, isPreset := closeReasons.Match(utils.ValueOrZero(current)); isPreset {
value = nil
}

return append(components, component.BuildLabel(component.Label{
Label: "Custom Reason",
Description: utils.Ptr("Only used if no reason is selected above"),
Component: component.BuildInputText(component.InputText{
Style: component.TextStyleParagraph,
CustomId: "reason",
Placeholder: utils.Ptr("No reason specified"),
MaxLength: utils.Ptr(uint32(1024)),
Required: utils.Ptr(false),
Value: value,
}),
}))
}
19 changes: 17 additions & 2 deletions bot/button/handlers/editclosereasonsubmit.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,21 @@ func (h *EditCloseReasonSubmitHandler) Execute(ctx *context.ModalContext) {
return
}

reason, ok := ctx.GetInput("reason")
if !ok {
reason, hasInput := ctx.GetInput("reason")
presets, hasSelect := ctx.GetValues("preset")
if !hasInput && !hasSelect {
ctx.HandleError(fmt.Errorf("reason input not found in modal submission"))
return
}

if hasSelect {
reason = pickCloseReason(reason, presets)
if reason == "" {
ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseReasonMissing)
return
}
}

if len(reason) > 1024 {
ctx.HandleError(fmt.Errorf("reason too long"))
return
Expand All @@ -85,6 +94,12 @@ func (h *EditCloseReasonSubmitHandler) Execute(ctx *context.ModalContext) {
return
}

reason, ok := logic.ResolveCloseReason(ctx.Context, ctx, ticket, reason)
if !ok {
ctx.Reply(customisation.Red, i18n.Error, i18n.MessageCloseReasonNotPredefined)
return
}

existing, _, err := dbclient.Client.CloseReason.Get(ctx.Context, guildId, ticketId)
if err != nil {
ctx.HandleError(err)
Expand Down
3 changes: 2 additions & 1 deletion bot/command/argument.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"github.com/TicketsBot-cloud/gdl/objects/interaction"
"github.com/TicketsBot-cloud/worker"
"github.com/TicketsBot-cloud/worker/i18n"
)

Expand All @@ -14,7 +15,7 @@ type Argument struct {
AutoCompleteHandler AutoCompleteHandler
}

type AutoCompleteHandler func(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice
type AutoCompleteHandler func(worker *worker.Context, data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice

func NewOptionalArgument(name, description string, argumentType interaction.ApplicationCommandOptionType, invalidMessage i18n.MessageId) Argument {
return Argument{
Expand Down
24 changes: 24 additions & 0 deletions bot/command/context/modalcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ func (c *ModalContext) GetInput(customId string) (string, bool) {
return "", false
}

func (c *ModalContext) GetValues(customId string) ([]string, bool) {
for _, c := range c.Interaction.Data.Components {
if (c.Type != component.ComponentActionRow && c.Type != component.ComponentLabel) || (len(c.Components) != 1 && c.Component == nil) {
continue
}

input := c.Component

if input == nil && len(c.Components) > 0 {
input = &c.Components[0]
}

if input.Type != component.ComponentSelectMenu {
continue
}

if input.CustomId == customId {
return input.Values, true
}
}

return nil, false
}

func (c *ModalContext) Worker() *worker.Context {
return c.worker
}
Expand Down
3 changes: 2 additions & 1 deletion bot/command/impl/admin/admingenpremium.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/TicketsBot-cloud/common/permission"
"github.com/TicketsBot-cloud/common/sentry"
"github.com/TicketsBot-cloud/gdl/objects/interaction"
"github.com/TicketsBot-cloud/worker"
"github.com/TicketsBot-cloud/worker/bot/command"
"github.com/TicketsBot-cloud/worker/bot/command/registry"
"github.com/TicketsBot-cloud/worker/bot/customisation"
Expand Down Expand Up @@ -119,7 +120,7 @@ func (AdminGenPremiumCommand) Execute(ctx registry.CommandContext, skuIdRaw stri
)))
}

func (AdminGenPremiumCommand) AutoCompleteHandler(data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice {
func (AdminGenPremiumCommand) AutoCompleteHandler(_ *worker.Context, data interaction.ApplicationCommandAutoCompleteInteraction, value string) []interaction.ApplicationCommandOptionChoice {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()

Expand Down
Loading
Loading