Skip to content
Open
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
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
Loading
Loading