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
10 changes: 10 additions & 0 deletions backend/app/http/endpoints/api/panel/panelcreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type panelBody struct {
SupportCanView bool `json:"support_can_view"`
SupportCanType bool `json:"support_can_type"`
AutoClose PanelAutoCloseBody `json:"auto_close"`
CloseReasons *database.PanelCloseReasons `json:"close_reasons"`
}

func (p *panelBody) IntoPanelMessageData(customId string, showBranding bool) panelMessageData {
Expand Down Expand Up @@ -145,6 +146,7 @@ func CreatePanel(c *gin.Context) {

// Apply defaults
ApplyPanelDefaults(&data)
normaliseCloseReasons(&data)

// Not in DefaultApplicators: that also runs on update, where an empty list means "delete
// every rule" rather than "unconfigured".
Expand Down Expand Up @@ -332,6 +334,7 @@ func CreatePanel(c *gin.Context) {
TeamIds: data.Teams, // Already validated
AccessControlRules: *data.AccessControlList, // Already validated
KBCategoryIds: data.KBCategoryIds, // Already validated
CloseReasons: data.CloseReasons,
}

// insert role mention data
Expand Down Expand Up @@ -399,6 +402,7 @@ type panelCreateOptions struct {
TeamIds []int
AccessControlRules []database.PanelAccessControlRule
KBCategoryIds []int
CloseReasons *database.PanelCloseReasons
}

func storePanel(ctx context.Context, panel database.Panel, options panelCreateOptions) (int, error) {
Expand Down Expand Up @@ -435,6 +439,12 @@ func storePanel(ctx context.Context, panel database.Panel, options panelCreateOp
return err
}

if options.CloseReasons != nil {
if err := dbclient.Client.PanelCloseReasons.SetWithTx(ctx, tx, panelId, *options.CloseReasons); err != nil {
return err
}
}

return nil
})

Expand Down
8 changes: 8 additions & 0 deletions backend/app/http/endpoints/api/panel/panelget.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func GetPanel(c *gin.Context) {
TicketPermissions database.TicketPermissions `json:"ticket_permissions"`
AutoClose PanelAutoCloseResponse `json:"auto_close"`
MentionBehaviour string `json:"mention_behaviour"`
CloseReasons database.PanelCloseReasons `json:"close_reasons"`
}

guildId := c.Keys["guildid"].(uint64)
Expand Down Expand Up @@ -164,6 +165,12 @@ func GetPanel(c *gin.Context) {
return
}

closeReasons, err := dbclient.Client.PanelCloseReasons.Get(c, panel.PanelId)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to load panel"))
return
}

mentionBehaviour := panel.MentionBehaviour
if mentionBehaviour == "" {
mentionBehaviour = "none"
Expand All @@ -184,6 +191,7 @@ func GetPanel(c *gin.Context) {
TicketPermissions: ticketPerms,
AutoClose: panelAutoCloseToResponse(autoCloseSettings),
MentionBehaviour: mentionBehaviour,
CloseReasons: closeReasons,
})
}

Expand Down
43 changes: 29 additions & 14 deletions backend/app/http/endpoints/api/panel/panellist.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,28 @@ import (

func ListPanels(c *gin.Context) {
type panelResponse struct {
PanelId int `json:"panel_id"`
ChannelId uint64 `json:"channel_id,string"`
Title string `json:"title"`
Colour int32 `json:"colour"`
ButtonLabel string `json:"button_label"`
ButtonStyle int `json:"button_style,string"`
EmojiName *string `json:"emoji_name,omitempty"`
EmojiId *uint64 `json:"emoji_id,omitempty,string"`
UseCustomEmoji bool `json:"use_custom_emoji"`
Emote *string `json:"emote,omitempty"`
HasSupportHours bool `json:"has_support_hours"`
IsCurrentlyActive bool `json:"is_currently_active"`
Disabled bool `json:"disabled"`
ForceDisabled bool `json:"force_disabled"`
PanelId int `json:"panel_id"`
ChannelId uint64 `json:"channel_id,string"`
Title string `json:"title"`
Colour int32 `json:"colour"`
ButtonLabel string `json:"button_label"`
ButtonStyle int `json:"button_style,string"`
EmojiName *string `json:"emoji_name,omitempty"`
EmojiId *uint64 `json:"emoji_id,omitempty,string"`
UseCustomEmoji bool `json:"use_custom_emoji"`
Emote *string `json:"emote,omitempty"`
HasSupportHours bool `json:"has_support_hours"`
IsCurrentlyActive bool `json:"is_currently_active"`
Disabled bool `json:"disabled"`
ForceDisabled bool `json:"force_disabled"`
CloseReasons database.PanelCloseReasons `json:"close_reasons"`
}

guildId := c.Keys["guildid"].(uint64)

var panels []database.Panel
var activePanelIds []int
var closeReasons map[int]database.PanelCloseReasons

g, ctx := errgroup.WithContext(c)

Expand All @@ -50,6 +52,12 @@ func ListPanels(c *gin.Context) {
return err
})

g.Go(func() error {
var err error
closeReasons, err = dbclient.Client.PanelCloseReasons.GetAllForGuild(ctx, guildId)
return err
})

if err := g.Wait(); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to load panels"))
return
Expand Down Expand Up @@ -110,6 +118,12 @@ func ListPanels(c *gin.Context) {
wrapped := make([]panelResponse, len(panels))
for i, p := range panels {
hasSH := supportHoursSet[p.PanelId]

panelCloseReasons, ok := closeReasons[p.PanelId]
if !ok {
panelCloseReasons = database.DefaultPanelCloseReasons()
}

wrapped[i] = panelResponse{
PanelId: p.PanelId,
ChannelId: p.ChannelId,
Expand All @@ -125,6 +139,7 @@ func ListPanels(c *gin.Context) {
IsCurrentlyActive: !hasSH || activeSet[p.PanelId],
Disabled: p.Disabled,
ForceDisabled: p.ForceDisabled,
CloseReasons: panelCloseReasons,
}
}

Expand Down
35 changes: 31 additions & 4 deletions backend/app/http/endpoints/api/panel/panelupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"

"github.com/TicketsBot-cloud/common/featureflags"
Expand Down Expand Up @@ -81,13 +82,26 @@ func UpdatePanel(c *gin.Context) {
return
}

var existingCloseReasons database.PanelCloseReasons
if data.CloseReasons != nil {
existingCloseReasons, err = dbclient.Client.PanelCloseReasons.Get(c, panelId)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, app.NewError(err, "Failed to load panel"))
return
}
}

if existing.ForceDisabled {
c.JSON(400, utils.ErrorStr("This panel is disabled and cannot be modified: please reactivate premium to re-enable it"))
return
}

// Apply defaults
ApplyPanelDefaults(&data)
normaliseCloseReasons(&data)

closeReasonsChanged := data.CloseReasons != nil &&
(data.CloseReasons.AllowCustom != existingCloseReasons.AllowCustom || !slices.Equal(data.CloseReasons.Reasons, existingCloseReasons.Reasons))

premiumTier, err := rpc.PremiumClient.GetTierByGuildId(c, guildId, true, botContext.Token, botContext.RateLimiter)
if err != nil {
Expand Down Expand Up @@ -371,6 +385,12 @@ func UpdatePanel(c *gin.Context) {
}
}

if closeReasonsChanged {
if err := dbclient.Client.PanelCloseReasons.SetWithTx(c, tx, panel.PanelId, *data.CloseReasons); err != nil {
return err
}
}

if err := dbclient.Client.PanelKBCategories.SetWithTx(c, tx, panel.PanelId, data.KBCategoryIds); err != nil {
return err
}
Expand Down Expand Up @@ -475,6 +495,16 @@ func UpdatePanel(c *gin.Context) {
}
}

metadata := map[string]any{
"access_control_list_old": existingAcl,
"access_control_list_new": data.AccessControlList,
}

if closeReasonsChanged {
metadata["close_reasons_old"] = existingCloseReasons
metadata["close_reasons_new"] = data.CloseReasons
}

audit.Log(audit.LogEntry{
GuildId: audit.Uint64Ptr(guildId),
UserId: userId,
Expand All @@ -483,10 +513,7 @@ func UpdatePanel(c *gin.Context) {
ResourceId: audit.StringPtr(strconv.Itoa(panelId)),
OldData: existing,
NewData: panel,
Metadata: map[string]any{
"access_control_list_old": existingAcl,
"access_control_list_new": data.AccessControlList,
},
Metadata: metadata,
})

c.JSON(200, utils.SuccessResponse)
Expand Down
48 changes: 48 additions & 0 deletions backend/app/http/endpoints/api/panel/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ func DefaultApplicators(data *panelBody) []defaults.DefaultApplicator {
}
}

func normaliseCloseReasons(data *panelBody) {
if data.CloseReasons == nil {
return
}

reasons := make([]string, 0, len(data.CloseReasons.Reasons))
for _, reason := range data.CloseReasons.Reasons {
if reason = strings.TrimSpace(reason); reason != "" {
reasons = append(reasons, reason)
}
}

data.CloseReasons.Reasons = reasons
}

const (
MentionBehaviourNone = "none"
MentionBehaviourHide = "hide"
Expand Down Expand Up @@ -82,6 +97,7 @@ func panelValidators() []validation.Validator[PanelValidationContext] {
validateNamingScheme,
validateWelcomeMessage,
validateAccessControlList,
validateCloseReasons,
validatePendingCategory,
validateTranscriptChannelId,
validateTicketNotificationChannel,
Expand Down Expand Up @@ -476,6 +492,38 @@ func validateAccessControlList(ctx PanelValidationContext) validation.Validation
}
}

func validateCloseReasons(ctx PanelValidationContext) validation.ValidationFunc {
return func() error {
if ctx.Data.CloseReasons == nil {
return nil
}

reasons := ctx.Data.CloseReasons.Reasons

if len(reasons) > 25 {
return validation.NewInvalidInputError("Panel cannot have more than 25 close reasons")
}

for i, reason := range reasons {
if utf8.RuneCountInString(reason) > 100 {
return validation.NewInvalidInputError("Close reasons must be 100 characters or fewer")
}

if len(reason) > 255 {
return validation.NewInvalidInputErrorf("Close reason \"%s\" is too long", reason)
}

for _, previous := range reasons[:i] {
if strings.EqualFold(previous, reason) {
return validation.NewInvalidInputErrorf("Duplicate close reason \"%s\"", reason)
}
}
}

return nil
}
}

func validateEmbed(e *types.CustomEmbed) error {
if e == nil {
return nil
Expand Down
Loading
Loading