Revamp settings page

This commit is contained in:
rxdn 2022-07-25 22:43:32 +01:00
parent 35658b2aaf
commit 59f4a247db
18 changed files with 566 additions and 457 deletions

View File

@ -1,67 +0,0 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"time"
)
// time.Duration marshals to nanoseconds, custom impl to marshal to seconds
type autoCloseBody struct {
Enabled bool `json:"enabled"`
SinceOpenWithNoResponse int64 `json:"since_open_with_no_response"`
SinceLastMessage int64 `json:"since_last_message"`
OnUserLeave bool `json:"on_user_leave"`
}
func GetAutoClose(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
settings, err := dbclient.Client.AutoClose.Get(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
ctx.JSON(200, convertToAutoCloseBody(settings))
}
func convertToAutoCloseBody(settings database.AutoCloseSettings) (body autoCloseBody) {
body.Enabled = settings.Enabled
if settings.SinceOpenWithNoResponse != nil {
body.SinceOpenWithNoResponse = int64(*settings.SinceOpenWithNoResponse / time.Second)
}
if settings.SinceLastMessage != nil {
body.SinceLastMessage = int64(*settings.SinceLastMessage / time.Second)
}
if settings.OnUserLeave != nil {
body.OnUserLeave = *settings.OnUserLeave
}
return
}
func convertFromAutoCloseBody(body autoCloseBody) (settings database.AutoCloseSettings) {
settings.Enabled = body.Enabled
if body.SinceOpenWithNoResponse > 0 {
duration := time.Second * time.Duration(body.SinceOpenWithNoResponse)
settings.SinceOpenWithNoResponse = &duration
}
if body.SinceLastMessage > 0 {
duration := time.Second * time.Duration(body.SinceLastMessage)
settings.SinceLastMessage = &duration
}
settings.OnUserLeave = &body.OnUserLeave
return
}

View File

@ -1,64 +0,0 @@
package api
import (
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/premium"
"github.com/gin-gonic/gin"
"time"
)
var maxDays = 90
var maxLength = time.Hour * 24 * time.Duration(maxDays)
func PostAutoClose(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var body autoCloseBody
if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
settings := convertFromAutoCloseBody(body)
// get premium
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if premiumTier < premium.Premium {
settings.SinceOpenWithNoResponse = nil
settings.SinceLastMessage = nil
}
// Time period cannot be negative, convertFromAutoCloseBody will not allow
if (settings.SinceOpenWithNoResponse != nil && *settings.SinceOpenWithNoResponse > maxLength) ||
(settings.SinceLastMessage != nil && *settings.SinceLastMessage > maxLength) {
ctx.JSON(400, utils.ErrorStr("Time period cannot be longer than %d days", maxDays))
return
}
if !settings.Enabled {
settings.SinceLastMessage = nil
settings.SinceOpenWithNoResponse = nil
settings.OnUserLeave = nil
}
if err := dbclient.Client.AutoClose.Set(guildId, settings); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -1,33 +0,0 @@
package customisation
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/worker/bot/customisation"
"github.com/gin-gonic/gin"
)
// GetColours TODO: Don't depend on worker
func GetColours(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
// TODO: Don't duplicate
raw, err := dbclient.Client.CustomColours.GetAll(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
colours := make(map[customisation.Colour]utils.HexColour)
for id, hex := range raw {
colours[customisation.Colour(id)] = utils.HexColour(hex)
}
for id, hex := range customisation.DefaultColours {
if _, ok := colours[id]; !ok {
colours[id] = utils.HexColour(hex)
}
}
ctx.JSON(200, colours)
}

View File

@ -1,71 +0,0 @@
package customisation
import (
"context"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/premium"
"github.com/TicketsBot/worker/bot/customisation"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
// UpdateColours TODO: Don't depend on worker
func UpdateColours(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Allow votes
premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if premiumTier < premium.Premium {
ctx.JSON(402, utils.ErrorStr("You must have premium to customise message appearance"))
return
}
var data map[customisation.Colour]utils.HexColour
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
if len(data) > len(customisation.DefaultColours) {
ctx.JSON(400, utils.ErrorStr("Invalid colour"))
return
}
for colourCode, hex := range customisation.DefaultColours {
if _, ok := data[colourCode]; !ok {
data[colourCode] = utils.HexColour(hex)
}
}
// TODO: Single query
group, _ := errgroup.WithContext(context.Background())
for colourCode, hex := range data {
colourCode := colourCode
hex := hex
group.Go(func() error {
return dbclient.Client.CustomColours.Set(guildId, colourCode.Int16(), hex.Int())
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -1,20 +0,0 @@
package api
import (
"github.com/TicketsBot/GoPanel/database"
"github.com/gin-gonic/gin"
)
func GetClaimSettings(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
settings, err := database.Client.ClaimSettings.Get(guildId); if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
ctx.JSON(200, settings)
}

View File

@ -3,13 +3,22 @@ package api
import ( import (
"context" "context"
dbclient "github.com/TicketsBot/GoPanel/database" dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database" "github.com/TicketsBot/database"
"github.com/TicketsBot/worker/bot/customisation"
"github.com/TicketsBot/worker/i18n"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"time"
) )
type Settings struct { type (
Settings struct {
database.Settings database.Settings
ClaimSettings database.ClaimSettings `json:"claim_settings"`
AutoCloseSettings AutoCloseData `json:"auto_close"`
Colours ColourMap `json:"colours"`
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
WelcomeMessage string `json:"welcome_message"` WelcomeMessage string `json:"welcome_message"`
TicketLimit uint8 `json:"ticket_limit"` TicketLimit uint8 `json:"ticket_limit"`
@ -20,8 +29,19 @@ type Settings struct {
UsersCanClose bool `json:"users_can_close"` UsersCanClose bool `json:"users_can_close"`
CloseConfirmation bool `json:"close_confirmation"` CloseConfirmation bool `json:"close_confirmation"`
FeedbackEnabled bool `json:"feedback_enabled"` FeedbackEnabled bool `json:"feedback_enabled"`
Language *i18n.Language `json:"language"`
} }
AutoCloseData struct {
Enabled bool `json:"enabled"`
SinceOpenWithNoResponse int64 `json:"since_open_with_no_response"`
SinceLastMessage int64 `json:"since_last_message"`
OnUserLeave bool `json:"on_user_leave"`
}
ColourMap map[customisation.Colour]utils.HexColour
)
func GetSettingsHandler(ctx *gin.Context) { func GetSettingsHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
@ -29,11 +49,35 @@ func GetSettingsHandler(ctx *gin.Context) {
group, _ := errgroup.WithContext(context.Background()) group, _ := errgroup.WithContext(context.Background())
// main settings
group.Go(func() (err error) { group.Go(func() (err error) {
settings.Settings, err = dbclient.Client.Settings.Get(guildId) settings.Settings, err = dbclient.Client.Settings.Get(guildId)
return return
}) })
// claim settings
group.Go(func() (err error) {
settings.ClaimSettings, err = dbclient.Client.ClaimSettings.Get(guildId)
return
})
// auto close settings
group.Go(func() error {
tmp, err := dbclient.Client.AutoClose.Get(guildId)
if err != nil {
return err
}
settings.AutoCloseSettings = convertToAutoCloseData(tmp)
return nil
})
// colour map
group.Go(func() (err error) {
settings.Colours, err = getColourMap(guildId)
return
})
// prefix // prefix
group.Go(func() (err error) { group.Go(func() (err error) {
settings.Prefix, err = dbclient.Client.Prefix.Get(guildId) settings.Prefix, err = dbclient.Client.Prefix.Get(guildId)
@ -106,13 +150,74 @@ func GetSettingsHandler(ctx *gin.Context) {
return return
}) })
if err := group.Wait(); err != nil { // language
ctx.AbortWithStatusJSON(500, gin.H{ group.Go(func() error {
"success": false, tmp, err := dbclient.Client.ActiveLanguage.Get(guildId)
"error": err.Error(), if err != nil {
return err
}
if tmp != "" {
settings.Language = utils.Ptr(i18n.Language(tmp))
}
return nil
}) })
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return return
} }
ctx.JSON(200, settings) ctx.JSON(200, struct {
Settings
Languages []i18n.Language `json:"languages"`
LanguageNames map[i18n.Language]string `json:"language_names"`
}{
Settings: settings,
Languages: i18n.LanguagesAlphabetical,
LanguageNames: i18n.FullNames,
})
}
func getColourMap(guildId uint64) (ColourMap, error) {
raw, err := dbclient.Client.CustomColours.GetAll(guildId)
if err != nil {
return nil, err
}
colours := make(ColourMap)
for id, hex := range raw {
if !utils.Exists(activeColours, customisation.Colour(id)) {
continue
}
colours[customisation.Colour(id)] = utils.HexColour(hex)
}
for _, id := range activeColours {
if _, ok := colours[id]; !ok {
colours[id] = utils.HexColour(customisation.DefaultColours[id])
}
}
return colours, nil
}
func convertToAutoCloseData(settings database.AutoCloseSettings) (body AutoCloseData) {
body.Enabled = settings.Enabled
if settings.SinceOpenWithNoResponse != nil {
body.SinceOpenWithNoResponse = int64(*settings.SinceOpenWithNoResponse / time.Second)
}
if settings.SinceLastMessage != nil {
body.SinceLastMessage = int64(*settings.SinceLastMessage / time.Second)
}
if settings.OnUserLeave != nil {
body.OnUserLeave = *settings.OnUserLeave
}
return
} }

View File

@ -1,40 +0,0 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
)
func PostClaimSettings(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var settings database.ClaimSettings
if err := ctx.BindJSON(&settings); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
if settings.SupportCanType && !settings.SupportCanView {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Must be able to view channel to type",
})
return
}
if err := dbclient.Client.ClaimSettings.Set(guildId, settings); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -2,14 +2,21 @@ package api
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database" dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/premium"
"github.com/TicketsBot/database" "github.com/TicketsBot/database"
"github.com/TicketsBot/worker/bot/customisation"
"github.com/TicketsBot/worker/i18n"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel" "github.com/rxdn/gdl/objects/channel"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"time"
) )
func UpdateSettingsHandler(ctx *gin.Context) { func UpdateSettingsHandler(ctx *gin.Context) {
@ -17,20 +24,52 @@ func UpdateSettingsHandler(ctx *gin.Context) {
var settings Settings var settings Settings
if err := ctx.BindJSON(&settings); err != nil { if err := ctx.BindJSON(&settings); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return return
} }
// Get a list of all channel IDs // Get a list of all channel IDs
channels := cache.Instance.GetGuildChannels(guildId) channels := cache.Instance.GetGuildChannels(guildId)
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Includes voting
premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if err := settings.Validate(guildId, premiumTier); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
group, _ := errgroup.WithContext(context.Background())
group.Go(func() error {
return settings.updateSettings(guildId)
})
group.Go(func() error {
return settings.updateClaimSettings(guildId)
})
addToWaitGroup(group, guildId, settings.updateLanguage)
addToWaitGroup(group, guildId, settings.updateAutoClose)
if premiumTier > premium.None {
addToWaitGroup(group, guildId, settings.updateColours)
}
// TODO: Errors // TODO: Errors
var errStr *string = nil var errStr *string = nil
if e := settings.updateSettings(guildId); e != nil { if err := group.Wait(); err != nil {
errStr = utils.Ptr(e.Error()) errStr = utils.Ptr(err.Error())
} }
validPrefix := settings.updatePrefix(guildId) validPrefix := settings.updatePrefix(guildId)
@ -56,16 +95,77 @@ func UpdateSettingsHandler(ctx *gin.Context) {
} }
func (s *Settings) updateSettings(guildId uint64) error { func (s *Settings) updateSettings(guildId uint64) error {
if err := s.Validate(guildId); err != nil {
return err
}
return dbclient.Client.Settings.Set(guildId, s.Settings) return dbclient.Client.Settings.Set(guildId, s.Settings)
} }
var validAutoArchive = []int{60, 1440, 4320, 10080} func (s *Settings) updateClaimSettings(guildId uint64) error {
return dbclient.Client.ClaimSettings.Set(guildId, s.ClaimSettings)
}
func (s *Settings) Validate(guildId uint64) error { var (
validAutoArchive = []int{60, 1440, 4320, 10080}
activeColours = []customisation.Colour{customisation.Green, customisation.Red}
)
func (s *Settings) Validate(guildId uint64, premiumTier premium.PremiumTier) error {
// Sync checks
if s.ClaimSettings.SupportCanType && !s.ClaimSettings.SupportCanView {
return errors.New("Must be able to view channel to type")
}
if s.Settings.UseThreads {
return fmt.Errorf("threads are disabled")
}
if s.Language != nil {
if _, ok := i18n.FullNames[*s.Language]; !ok {
return fmt.Errorf("invalid language")
}
}
// Validate colours
if len(s.Colours) > len(activeColours) {
return errors.New("Invalid colour")
}
for colour, _ := range s.Colours {
if !utils.Exists(activeColours, colour) {
return errors.New("Invalid colour")
}
}
for _, colourCode := range activeColours {
if _, ok := s.Colours[colourCode]; !ok {
s.Colours[colourCode] = utils.HexColour(customisation.DefaultColours[colourCode])
}
}
// Validate autoclose
if premiumTier < premium.Premium {
s.AutoCloseSettings.SinceOpenWithNoResponse = 0
s.AutoCloseSettings.SinceLastMessage = 0
}
if !s.AutoCloseSettings.Enabled {
s.AutoCloseSettings.SinceOpenWithNoResponse = 0
s.AutoCloseSettings.SinceLastMessage = 0
s.AutoCloseSettings.OnUserLeave = false
}
if s.AutoCloseSettings.SinceOpenWithNoResponse < 0 {
s.AutoCloseSettings.SinceOpenWithNoResponse = 0
}
if s.AutoCloseSettings.SinceLastMessage < 0 {
s.AutoCloseSettings.SinceLastMessage = 0
}
if s.AutoCloseSettings.SinceLastMessage > int64((time.Hour*24*60).Seconds()) ||
s.AutoCloseSettings.SinceOpenWithNoResponse > int64((time.Hour*24*60).Seconds()) {
return errors.New("Autoclose time period cannot be longer than 60 days")
}
// Async checks
group, _ := errgroup.WithContext(context.Background()) group, _ := errgroup.WithContext(context.Background())
// Validate panel from same guild // Validate panel from same guild
@ -102,14 +202,6 @@ func (s *Settings) Validate(guildId uint64) error {
return nil return nil
}) })
group.Go(func() error {
if s.Settings.UseThreads {
return fmt.Errorf("threads are disabled")
} else {
return nil
}
})
group.Go(func() error { group.Go(func() error {
if s.Settings.OverflowCategoryId != nil { if s.Settings.OverflowCategoryId != nil {
ch, ok := cache.Instance.GetChannel(*s.Settings.OverflowCategoryId) ch, ok := cache.Instance.GetChannel(*s.Settings.OverflowCategoryId)
@ -132,6 +224,12 @@ func (s *Settings) Validate(guildId uint64) error {
return group.Wait() return group.Wait()
} }
func addToWaitGroup(group *errgroup.Group, guildId uint64, f func(uint64) error) {
group.Go(func() error {
return f(guildId)
})
}
func (s *Settings) updatePrefix(guildId uint64) bool { func (s *Settings) updatePrefix(guildId uint64) bool {
if s.Prefix == "" || len(s.Prefix) > 8 { if s.Prefix == "" || len(s.Prefix) > 8 {
return false return false
@ -232,3 +330,43 @@ func (s *Settings) updateCloseConfirmation(guildId uint64) {
func (s *Settings) updateFeedbackEnabled(guildId uint64) { func (s *Settings) updateFeedbackEnabled(guildId uint64) {
go dbclient.Client.FeedbackEnabled.Set(guildId, s.FeedbackEnabled) go dbclient.Client.FeedbackEnabled.Set(guildId, s.FeedbackEnabled)
} }
func (s *Settings) updateLanguage(guildId uint64) error {
if s.Language == nil {
return dbclient.Client.ActiveLanguage.Delete(guildId)
} else {
return dbclient.Client.ActiveLanguage.Set(guildId, string(*s.Language))
}
}
func (s *Settings) updateColours(guildId uint64) error {
// Convert ColourMap to primitives
converted := make(map[int16]int)
for colour, hex := range s.Colours {
converted[int16(colour)] = int(hex)
}
return dbclient.Client.CustomColours.BatchSet(guildId, converted)
}
func (s *Settings) updateAutoClose(guildId uint64) error {
data := s.AutoCloseSettings.ConvertToDatabase() // Already validated
return dbclient.Client.AutoClose.Set(guildId, data)
}
func (d AutoCloseData) ConvertToDatabase() (settings database.AutoCloseSettings) {
settings.Enabled = d.Enabled
if d.SinceOpenWithNoResponse > 0 {
duration := time.Second * time.Duration(d.SinceOpenWithNoResponse)
settings.SinceOpenWithNoResponse = &duration
}
if d.SinceLastMessage > 0 {
duration := time.Second * time.Duration(d.SinceLastMessage)
settings.SinceLastMessage = &duration
}
settings.OnUserLeave = &d.OnUserLeave
return
}

View File

@ -3,9 +3,7 @@ package http
import ( import (
"github.com/TicketsBot/GoPanel/app/http/endpoints/api" "github.com/TicketsBot/GoPanel/app/http/endpoints/api"
"github.com/TicketsBot/GoPanel/app/http/endpoints/api/admin/botstaff" "github.com/TicketsBot/GoPanel/app/http/endpoints/api/admin/botstaff"
api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose"
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist" api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
api_customisation "github.com/TicketsBot/GoPanel/app/http/endpoints/api/customisation"
api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms" api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms"
api_integrations "github.com/TicketsBot/GoPanel/app/http/endpoints/api/integrations" api_integrations "github.com/TicketsBot/GoPanel/app/http/endpoints/api/integrations"
api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel" api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
@ -149,15 +147,6 @@ func StartServer() {
guildAuthApiSupport.PUT("/tags", api_tags.CreateTag) guildAuthApiSupport.PUT("/tags", api_tags.CreateTag)
guildAuthApiSupport.DELETE("/tags", api_tags.DeleteTag) guildAuthApiSupport.DELETE("/tags", api_tags.DeleteTag)
guildAuthApiAdmin.GET("/claimsettings", api_settings.GetClaimSettings)
guildAuthApiAdmin.POST("/claimsettings", api_settings.PostClaimSettings)
guildAuthApiAdmin.GET("/autoclose", api_autoclose.GetAutoClose)
guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose)
guildAuthApiAdmin.GET("/customisation/colours", api_customisation.GetColours)
guildAuthApiAdmin.POST("/customisation/colours", api_customisation.UpdateColours)
guildAuthApiAdmin.GET("/team", api_team.GetTeams) guildAuthApiAdmin.GET("/team", api_team.GetTeams)
guildAuthApiAdmin.GET("/team/:teamid", rl(middleware.RateLimitTypeUser, 10, time.Second*30), api_team.GetMembers) guildAuthApiAdmin.GET("/team/:teamid", rl(middleware.RateLimitTypeUser, 10, time.Second*30), api_team.GetMembers)
guildAuthApiAdmin.POST("/team", rl(middleware.RateLimitTypeUser, 10, time.Minute), api_team.CreateTeam) guildAuthApiAdmin.POST("/team", rl(middleware.RateLimitTypeUser, 10, time.Minute), api_team.CreateTeam)

View File

@ -16,17 +16,24 @@
</div> </div>
</div> </div>
<svelte:window bind:innerWidth />
<script> <script>
import {onMount} from "svelte"; import {onMount} from "svelte";
export let retractIcon = "fas fa-minus"; export let retractIcon = "fas fa-minus";
export let expandIcon = "fas fa-plus"; export let expandIcon = "fas fa-plus";
export let defaultOpen = false;
let expanded = false; let expanded = false;
let showOverflow = true; let showOverflow = true;
let content; let content;
let innerWidth;
$: innerWidth, updateIfExpanded();
export function toggle() { export function toggle() {
if (expanded) { if (expanded) {
content.style.maxHeight = 0; content.style.maxHeight = 0;
@ -41,15 +48,17 @@
content.style.maxHeight = `${content.scrollHeight}px`; content.style.maxHeight = `${content.scrollHeight}px`;
} }
onMount(() => { function updateIfExpanded() {
const fn = (e) => {
if (expanded) { if (expanded) {
updateSize(); updateSize();
} }
} }
content.addEventListener('DOMNodeInserted', fn); onMount(() => {
content.addEventListener('DOMNodeRemoved', fn); content.addEventListener('DOMNodeInserted', updateIfExpanded);
content.addEventListener('DOMNodeRemoved', updateIfExpanded);
if (defaultOpen) toggle();
}); });
</script> </script>
@ -76,5 +85,6 @@
border-left: 0; border-left: 0;
border-right: 0; border-right: 0;
width: 100%; width: 100%;
flex: 1;
} }
</style> </style>

View File

@ -0,0 +1,19 @@
<Badge>
<div class="inner">
<i class="fas fa-gem"></i>
<span>Premium</span>
</div>
</Badge>
<script>
import Badge from "./Badge.svelte";
</script>
<style>
.inner {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
</style>

View File

@ -1,11 +1,12 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}> <div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
<label for="input" class="form-label">{label}</label> <label for="input" class="form-label">{label}</label>
<input id="input" class="form-checkbox" type=checkbox bind:checked={value} on:change> <input id="input" class="form-checkbox" type=checkbox bind:checked={value} on:change {disabled}>
</div> </div>
<script> <script>
export let value; export let value;
export let label; export let label;
export let disabled = false;
export let col1 = false; export let col1 = false;
export let col2 = false; export let col2 = false;

View File

@ -1,11 +1,6 @@
<div class="col"> <div class="col">
<div class="row label"> <div class="row label">
<div class="parent"> <slot name="header"></slot>
<label class="form-label">{label}</label>
{#if badge !== undefined}
<Badge>{badge}</Badge>
{/if}
</div>
</div> </div>
<div class="row fields"> <div class="row fields">
@ -27,10 +22,6 @@
</div> </div>
<script> <script>
import Badge from "../Badge.svelte";
export let label;
export let badge;
export let disabled = false; // note: bind:disabled isn't valid export let disabled = false; // note: bind:disabled isn't valid
export let days = 0; export let days = 0;
@ -88,6 +79,12 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
.header-wrapper {
display: flex;
flex-direction: row;
gap: 4px;
}
.parent { .parent {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -1,3 +1,4 @@
{#if data}
<Card footer="{false}" fill="{false}"> <Card footer="{false}" fill="{false}">
<span slot="title"> <span slot="title">
Settings Settings
@ -5,19 +6,33 @@
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<form class="settings-form" on:submit|preventDefault={updateSettings}> <form class="settings-form" on:submit|preventDefault={updateSettings}>
<Collapsible defaultOpen>
<span slot="header">General</span>
<div slot="content" class="col-1">
<div class="row"> <div class="row">
<Input label="prefix (max len. 8)" placeholder="t!" col4=true bind:value={data.prefix}/> <Input label="prefix (max len. 8)" placeholder="t!" col4 bind:value={data.prefix}/>
<Number label="per user simultaneous ticket limit" col4=true min=1 max=10 bind:value={data.ticket_limit}/> <Number label="per user simultaneous ticket limit" min=1 max=10 bind:value={data.ticket_limit}/>
<Checkbox label="allow users to close tickets" col4=true bind:value={data.users_can_close}/> <Dropdown label="Language" bind:value={data.language}>
<Checkbox label="ticket close confirmation" col4=true bind:value={data.close_confirmation}/> <option value=null selected="selected">Server Default</option>
{#if data.languages}
{#each data.languages as language}
<option value={language}>{data.language_names[language]}</option>
{/each}
{/if}
</Dropdown>
<Checkbox label="allow users to close tickets" bind:value={data.users_can_close}/>
<Checkbox label="ticket close confirmation" bind:value={data.close_confirmation}/>
<Checkbox label="Enable User Feedback" bind:value={data.feedback_enabled}/>
</div> </div>
<div class="row">
<Textarea label="welcome message" placeholder="Thanks for opening a ticket!" col1=true
bind:value={data.welcome_message}/>
</div> </div>
</Collapsible>
<Collapsible defaultOpen>
<span slot="header">Tickets</span>
<div slot="content" class="col-1">
<div class="row"> <div class="row">
<ChannelDropdown label="Archive Channel" col3=true channels={channels} withNull={true} bind:value={data.archive_channel}/> <ChannelDropdown label="Archive Channel" col3=true channels={channels} withNull={true}
<CategoryDropdown label="Channel Category" col3=true channels={channels} bind:value={data.category}/> bind:value={data.archive_channel}/>
<Dropdown label="Overflow Category" col3=true bind:value={data.overflow_category_id}> <Dropdown label="Overflow Category" col3=true bind:value={data.overflow_category_id}>
<option value=-1>Disabled</option> <option value=-1>Disabled</option>
<option value=-2>Uncategorised (Appears at top of channel list)</option> <option value=-2>Uncategorised (Appears at top of channel list)</option>
@ -29,30 +44,105 @@
{/if} {/if}
{/each} {/each}
</Dropdown> </Dropdown>
<Checkbox label="Store Ticket Transcripts" bind:value={data.store_transcripts}/>
<Checkbox label="Hide Claim Button" bind:value={data.hide_claim_button}/>
</div>
<div class="row">
</div>
</div>
</Collapsible>
<Collapsible>
<span slot="header">/Open Command</span>
<div slot="content" class="col-1">
<div class="row">
<Checkbox label="Disable /open Command" bind:value={data.disable_open_command}/>
<CategoryDropdown label="Channel Category" col3 channels={channels} bind:value={data.category}/>
<NamingScheme bind:value={data.naming_scheme}/>
</div> </div>
<div class="row"> <div class="row">
<NamingScheme col4=true bind:value={data.naming_scheme}/> <div class="col-1-flex">
<Checkbox label="Enable User Feedback" col4=true bind:value={data.feedback_enabled}/> <Textarea label="welcome message" placeholder="Thanks for opening a ticket!" col1
<Checkbox label="Hide Claim Button" col4=true bind:value={data.hide_claim_button}/> bind:value={data.welcome_message}/>
<Checkbox label="Disable /open Command" col4=true bind:value={data.disable_open_command}/>
</div> </div>
<div class="row">
<Checkbox label="Store Ticket Transcripts" col4=true bind:value={data.store_transcripts}/>
</div> </div>
<div class="from-message-settings"> </div>
<h3>Start Ticket From Message Settings</h3> </Collapsible>
<Collapsible>
<span slot="header">Context Menu (Start Ticket Dropdown)</span>
<div slot="content" class="col-1">
<div class="row"> <div class="row">
<Dropdown col3={true} label="Required Permission Level" bind:value={data.context_menu_permission_level}> <Dropdown col3 label="Required Permission Level" bind:value={data.context_menu_permission_level}>
<option value="0">Everyone</option> <option value="0">Everyone</option>
<option value="1">Support Representative</option> <option value="1">Support Representative</option>
<option value="2">Administrator</option> <option value="2">Administrator</option>
</Dropdown> </Dropdown>
<Checkbox label="Add Message Sender To Ticket" col3={true} bind:value={data.context_menu_add_sender}/> <Checkbox label="Add Message Sender To Ticket" bind:value={data.context_menu_add_sender}/>
<SimplePanelDropdown label="Use Settings From Panel" col3={true} allowNone={true} bind:panels <SimplePanelDropdown label="Use Settings From Panel" col3 allowNone={true} bind:panels
bind:value={data.context_menu_panel}/> bind:value={data.context_menu_panel}/>
</div> </div>
</div> </div>
</Collapsible>
<Collapsible>
<span slot="header">Claiming</span>
<div slot="content" class="col-1">
<div class="row">
<Checkbox label="SUPPORT REPS CAN VIEW CLAIMED TICKETS" bind:value={data.claim_settings.support_can_view}
on:change={validateView}/>
<Checkbox label="SUPPORT REPS CAN TYPE IN CLAIMED TICKETS" bind:value={data.claim_settings.support_can_type}
on:change={validateType}/>
</div>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Auto Close</span>
<div slot="content" class="col-1">
<div class="row">
<Checkbox label="Enabled" bind:value={data.auto_close.enabled}/>
<Checkbox label="Close On User Leave" disabled={!data.auto_close.enabled} bind:value={data.auto_close.on_user_leave}/>
</div>
<div class="row" style="justify-content: space-between">
<div class="col-2" style="flex-direction: row">
<Duration disabled={!isPremium || !data.auto_close.enabled} bind:days={sinceOpenDays} bind:hours={sinceOpenHours}
bind:minutes={sinceOpenMinutes}>
<div slot="header" class="header">
<label class="form-label" style="margin-bottom: unset">Since Open With No Response</label>
<PremiumBadge/>
</div>
</Duration>
</div>
<div class="col-2" style="flex-direction: row">
<Duration disabled={!isPremium || !data.auto_close.enabled} bind:days={sinceLastDays} bind:hours={sinceLastHours}
bind:minutes={sinceLastMinutes}>
<div slot="header" class="header">
<label class="form-label" style="margin-bottom: unset">Since Last Message</label>
<PremiumBadge/>
</div>
</Duration>
</div>
</div>
</div>
</Collapsible>
<Collapsible>
<div slot="header" class="header">
<span>Colour Scheme</span>
<PremiumBadge/>
</div>
<div slot="content" class="col-1">
<div class="row">
<Colour col4 label="Success" bind:value={data.colours["0"]} disabled={!isPremium}/>
<Colour col4 label="Failure" bind:value={data.colours["1"]} disabled={!isPremium}/>
</div>
</div>
</Collapsible>
<div class="row"> <div class="row">
<div class="col-1"> <div class="col-1">
<Button icon="fas fa-paper-plane" fullWidth=true>Submit</Button> <Button icon="fas fa-paper-plane" fullWidth=true>Submit</Button>
@ -61,6 +151,15 @@
</form> </form>
</div> </div>
</Card> </Card>
{/if}
<svelte:head>
<style>
body {
overflow-y: scroll;
}
</style>
</svelte:head>
<script> <script>
import ChannelDropdown from "../ChannelDropdown.svelte"; import ChannelDropdown from "../ChannelDropdown.svelte";
@ -79,6 +178,11 @@
import NamingScheme from "../NamingScheme.svelte"; import NamingScheme from "../NamingScheme.svelte";
import Dropdown from "../form/Dropdown.svelte"; import Dropdown from "../form/Dropdown.svelte";
import SimplePanelDropdown from "../SimplePanelDropdown.svelte"; import SimplePanelDropdown from "../SimplePanelDropdown.svelte";
import Collapsible from "../Collapsible.svelte";
import Duration from "../form/Duration.svelte";
import Colour from "../form/Colour.svelte";
import PremiumBadge from "../PremiumBadge.svelte";
import {toDays, toHours, toMinutes} from "../../js/timeutil";
export let guildId; export let guildId;
@ -86,6 +190,24 @@
let channels = []; let channels = [];
let panels = []; let panels = [];
let isPremium = false;
let data;
let sinceOpenDays = 0, sinceOpenHours = 0, sinceOpenMinutes = 0;
let sinceLastDays = 0, sinceLastHours = 0, sinceLastMinutes = 0;
function validateView() {
if (!data.support_can_view && data.support_can_type) {
data.support_can_type = false;
}
}
function validateType() {
if (!data.support_can_view && data.support_can_type) {
data.support_can_view = true;
}
}
async function loadPanels() { async function loadPanels() {
const res = await axios.get(`${API_URL}/api/${guildId}/panels`); const res = await axios.get(`${API_URL}/api/${guildId}/panels`);
@ -107,11 +229,15 @@
channels = res.data; channels = res.data;
} }
let data = { async function loadPremium() {
ticket_limit: 5, const res = await axios.get(`${API_URL}/api/${guildId}/premium`);
users_can_close: true, if (res.status !== 200) {
close_confirmation: true, notifyError(res.data.error);
}; return;
}
isPremium = res.data.premium;
}
async function updateSettings() { async function updateSettings() {
// Svelte hack - I can't even remember what this does // Svelte hack - I can't even remember what this does
@ -135,6 +261,10 @@
mapped.overflow_enabled = true; mapped.overflow_enabled = true;
} }
// Normalise autoclose
data.auto_close.since_open_with_no_response = sinceOpenDays * 86400 + sinceOpenHours * 3600 + sinceOpenMinutes * 60;
data.auto_close.since_last_message = sinceLastDays * 86400 + sinceLastHours * 3600 + sinceLastMinutes * 60;
const res = await axios.post(`${API_URL}/api/${guildId}/settings`, mapped); const res = await axios.post(`${API_URL}/api/${guildId}/settings`, mapped);
if (res.status === 200) { if (res.status === 200) {
if (showValidations(res.data)) { if (showValidations(res.data)) {
@ -224,12 +354,30 @@
data.overflow_category_id = "-2"; data.overflow_category_id = "-2";
} }
} }
if (data.language === null) {
data.language = "null";
}
// Auto close overrides
if (data.auto_close.since_open_with_no_response) {
sinceOpenDays = toDays(data.auto_close.since_open_with_no_response);
sinceOpenHours = toHours(data.auto_close.since_open_with_no_response);
sinceOpenMinutes = toMinutes(data.auto_close.since_open_with_no_response);
}
if (data.auto_close.since_last_message) {
sinceLastDays = toDays(data.auto_close.since_last_message);
sinceLastHours = toHours(data.auto_close.since_last_message);
sinceLastMinutes = toMinutes(data.auto_close.since_last_message);
}
} }
withLoadingScreen(async () => { withLoadingScreen(async () => {
await Promise.all([ await Promise.all([
loadPanels(), loadPanels(),
loadChannels() loadChannels(),
loadPremium()
]); ]);
await loadData(); // Depends on channels await loadData(); // Depends on channels
@ -245,7 +393,9 @@
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: flex-start;
flex-wrap: wrap;
gap: 2%;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin-top: 10px; margin-top: 10px;
@ -258,10 +408,11 @@
height: 100%; height: 100%;
} }
.from-message-settings { .col-1-flex {
border-top: 1px solid rgba(0, 0, 0, .25); display: flex;
margin-top: 25px; flex-direction: column;
padding-top: 10px; align-items: flex-start;
flex: 0 0 100%;
} }
@media only screen and (max-width: 950px) { @media only screen and (max-width: 950px) {
@ -306,4 +457,11 @@
width: 23%; width: 23%;
height: 100%; height: 100%;
} }
.header {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
</style> </style>

View File

@ -30,14 +30,11 @@
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement> <NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
<NavElement icon="fas fa-ban" link="/manage/{guildId}/blacklist" on:click={closeDropdown}>Blacklist</NavElement> <NavElement icon="fas fa-ban" link="/manage/{guildId}/blacklist" on:click={closeDropdown}>Blacklist</NavElement>
<NavElement icon="fas fa-tags" link="/manage/{guildId}/tags" on:click={closeDropdown}>Tags</NavElement> <NavElement icon="fas fa-tags" link="/manage/{guildId}/tags" on:click={closeDropdown}>Tags</NavElement>
{#if isAdmin}
<NavElement icon="fas fa-paint-brush" link="/manage/{guildId}/appearance" on:click={closeDropdown}>Appearance</NavElement>
{/if}
</div> </div>
</div> </div>
<div> <div>
<div class="nav-section" class:dropdown={$dropdown}> <div class="nav-section" class:dropdown={$dropdown}>
<NavElement icon="fas fa-book" link="https://docs.ticketsbot.net">Documentation</NavElement>
<NavElement icon="fas fa-server" link="/#">Servers</NavElement> <NavElement icon="fas fa-server" link="/#">Servers</NavElement>
<NavElement icon="fas fa-sign-out-alt" link="/logout">Logout</NavElement> <NavElement icon="fas fa-sign-out-alt" link="/logout">Logout</NavElement>
</div> </div>

View File

@ -3,23 +3,11 @@
<div class="card"> <div class="card">
<SettingsCard {guildId}/> <SettingsCard {guildId}/>
</div> </div>
<div class="card">
<AutoCloseCard {guildId}/>
</div>
</div>
<div class="right-col">
<div class="card">
<ClaimsCard {guildId}/>
</div>
</div> </div>
</div> </div>
<script> <script>
import SettingsCard from "../components/manage/SettingsCard.svelte"; import SettingsCard from "../components/manage/SettingsCard.svelte";
import AutoCloseCard from "../components/manage/AutoCloseCard.svelte";
import ClaimsCard from "../components/manage/ClaimsCard.svelte";
import {onMount} from "svelte";
import {dropdown} from "../js/stores";
export let currentRoute; export let currentRoute;
let guildId = currentRoute.namedParams.id let guildId = currentRoute.namedParams.id
@ -39,7 +27,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 66.6%; width: 100%;
margin-top: 30px; margin-top: 30px;
} }

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
github.com/TicketsBot/database v0.0.0-20220723212053-ab122ba82749 github.com/TicketsBot/database v0.0.0-20220725214217-fe953c05126d
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20220710121124-cd5ec72739f9 github.com/TicketsBot/worker v0.0.0-20220710121124-cd5ec72739f9
github.com/apex/log v1.1.2 github.com/apex/log v1.1.2

2
go.sum
View File

@ -7,6 +7,8 @@ github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqS
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM= github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
github.com/TicketsBot/database v0.0.0-20220723212053-ab122ba82749 h1:U/TnoBH3AyeV8uuQK/g69NfdNzGYGnjMD5KryJ+93Ok= github.com/TicketsBot/database v0.0.0-20220723212053-ab122ba82749 h1:U/TnoBH3AyeV8uuQK/g69NfdNzGYGnjMD5KryJ+93Ok=
github.com/TicketsBot/database v0.0.0-20220723212053-ab122ba82749/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw= github.com/TicketsBot/database v0.0.0-20220723212053-ab122ba82749/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/database v0.0.0-20220725214217-fe953c05126d h1:Xjlg6CHM+rXl5kWJevZspK5SxcGtaY3xE1q64/MDkx8=
github.com/TicketsBot/database v0.0.0-20220725214217-fe953c05126d/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=