multipanels

This commit is contained in:
rxdn 2020-07-23 15:05:10 +01:00
parent e61295dd3c
commit 189ec33874
42 changed files with 1036 additions and 100 deletions

View File

@ -0,0 +1,236 @@
package api
import (
"context"
"errors"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/premium"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/channel/embed"
"github.com/rxdn/gdl/objects/channel/message"
"github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request"
gdlutils "github.com/rxdn/gdl/utils"
"golang.org/x/sync/errgroup"
)
type multiPanelCreateData struct {
Title string `json:"title"`
Content string `json:"content"`
Colour int32 `json:"colour"`
ChannelId uint64 `json:"channel_id,string"`
Panels gdlutils.Uint64StringSlice `json:"panels"`
}
func MultiPanelCreate(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data multiPanelCreateData
if err := ctx.ShouldBindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// validate body & get sub-panels
panels, err := data.doValidations(guildId)
if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// get bot context
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// get premium status
premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
messageId, err := data.sendEmbed(&botContext, premiumTier > premium.None)
if err != nil {
if err == request.ErrForbidden {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to send messages in the provided channel")))
} else {
ctx.JSON(500, utils.ErrorToResponse(err))
}
return
}
if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil {
if err == request.ErrForbidden {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to add reactions in the provided channel")))
} else {
ctx.JSON(500, utils.ErrorToResponse(err))
}
return
}
multiPanel := database.MultiPanel{
MessageId: messageId,
ChannelId: data.ChannelId,
GuildId: guildId,
Title: data.Title,
Content: data.Content,
Colour: int(data.Colour),
}
multiPanel.Id, err = dbclient.Client.MultiPanels.Create(multiPanel)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
group, _ := errgroup.WithContext(context.Background())
for _, panel := range panels {
panel := panel
group.Go(func() error {
return dbclient.Client.MultiPanelTargets.Insert(multiPanel.Id, panel.MessageId)
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
ctx.JSON(200, gin.H{
"success": true,
"data": multiPanel,
})
}
func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database.Panel, err error) {
group, _ := errgroup.WithContext(context.Background())
group.Go(d.validateTitle)
group.Go(d.validateContent)
group.Go(d.validateChannel(guildId))
group.Go(func() (e error) {
panels, e = d.validatePanels(guildId)
return
})
err = group.Wait()
return
}
func (d *multiPanelCreateData) validateTitle() (err error) {
if len(d.Title) > 255 || len(d.Title) < 1 {
err = errors.New("embed title must be between 1 and 255 characters")
}
return
}
func (d *multiPanelCreateData) validateContent() (err error) {
if len(d.Content) > 1024 || len(d.Title) < 1 {
err = errors.New("embed content must be between 1 and 1024 characters")
}
return
}
func (d *multiPanelCreateData) validateChannel(guildId uint64) func() error {
return func() (err error) {
channels := cache.Instance.GetGuildChannels(guildId)
var valid bool
for _, ch := range channels {
if ch.Id == d.ChannelId && ch.Type == channel.ChannelTypeGuildText {
valid = true
break
}
}
if !valid {
err = errors.New("channel does not exist")
}
return
}
}
func (d *multiPanelCreateData) validatePanels(guildId uint64) (panels []database.Panel, err error) {
if len(d.Panels) < 2 {
err = errors.New("a multi-panel must contain at least 2 sub-panels")
return
}
if len(d.Panels) > 15 {
err = errors.New("multi-panels cannot contain more than 15 sub-panels")
return
}
existingPanels, err := dbclient.Client.Panel.GetByGuild(guildId)
if err != nil {
return nil, err
}
for _, panelId := range d.Panels {
var valid bool
// find panel struct
for _, panel := range existingPanels {
if panel.MessageId == panelId {
// check there isn't a panel with the same reaction emote
for _, previous := range panels {
if previous.ReactionEmote == panel.ReactionEmote {
return nil, errors.New("2 sub-panels cannot have the same reaction emotes")
}
}
valid = true
panels = append(panels, panel)
}
}
if !valid {
return nil, errors.New("invalid panel ID")
}
}
return
}
func (d *multiPanelCreateData) sendEmbed(ctx *botcontext.BotContext, isPremium bool) (messageId uint64, err error) {
e := embed.NewEmbed().
SetTitle(d.Title).
SetDescription(d.Content).
SetColor(int(d.Colour))
if !isPremium {
// TODO: Don't harcode
e.SetFooter("Powered by ticketsbot.net", "https://cdn.discordapp.com/avatars/508391840525975553/ac2647ffd4025009e2aa852f719a8027.png?size=256")
}
var msg message.Message
msg, err = rest.CreateMessage(ctx.Token, ctx.RateLimiter, d.ChannelId, rest.CreateMessageData{Embed: e})
if err != nil {
return
}
messageId = msg.Id
return
}
func (d *multiPanelCreateData) addReactions(ctx *botcontext.BotContext, channelId, messageId uint64, panels []database.Panel) (err error) {
for _, panel := range panels {
if err = rest.CreateReaction(ctx.Token, ctx.RateLimiter, channelId, messageId, panel.ReactionEmote); err != nil {
return err
}
}
return
}

View File

@ -0,0 +1,58 @@
package api
import (
"errors"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request"
"strconv"
)
func MultiPanelDelete(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
multiPanelId, err := strconv.Atoi(ctx.Param("panelid"))
if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// get bot context
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
panel, ok, err := dbclient.Client.MultiPanels.Get(multiPanelId)
if !ok {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with matching ID found")))
return
}
if panel.GuildId != guildId {
ctx.JSON(403, utils.ErrorToResponse(errors.New("Guild ID doesn't match")))
return
}
if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, panel.ChannelId, panel.MessageId); err != nil && !request.IsClientError(err) {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
success, err := dbclient.Client.MultiPanels.Delete(guildId, multiPanelId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
if !success {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with matching ID found")))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,56 @@
package api
import (
"context"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
func MultiPanelList(ctx *gin.Context) {
type multiPanelResponse struct {
database.MultiPanel
Panels []database.Panel `json:"panels"`
}
guildId := ctx.Keys["guildid"].(uint64)
multiPanels, err := dbclient.Client.MultiPanels.GetByGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
data := make([]multiPanelResponse, len(multiPanels))
group, _ := errgroup.WithContext(context.Background())
for i, multiPanel := range multiPanels {
i := i
multiPanel := multiPanel
data[i] = multiPanelResponse{
MultiPanel: multiPanel,
}
group.Go(func() error {
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
if err != nil {
return err
}
data[i].Panels = panels
return nil
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
ctx.JSON(200, gin.H{
"success": true,
"data": data,
})
}

View File

@ -0,0 +1,143 @@
package api
import (
"context"
"errors"
"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/database"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request"
"golang.org/x/sync/errgroup"
"strconv"
)
func MultiPanelUpdate(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
// parse body
var data multiPanelCreateData
if err := ctx.ShouldBindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// parse panel ID
panelId, err := strconv.Atoi(ctx.Param("panelid"))
if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// retrieve panel from DB
multiPanel, ok, err := dbclient.Client.MultiPanels.Get(panelId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
// check panel exists
if !ok {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with the provided ID found")))
return
}
// check panel is in the same guild
if guildId != multiPanel.GuildId {
ctx.JSON(403, utils.ErrorToResponse(errors.New("Guild ID doesn't match")))
return
}
// validate body & get sub-panels
panels, err := data.doValidations(guildId)
if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err))
return
}
// get bot context
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
// delete old message
if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId); err != nil && !request.IsClientError(err) {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
// get premium status
premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
// send new message
messageId, err := data.sendEmbed(&botContext, premiumTier > premium.None)
if err != nil {
if err == request.ErrForbidden {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to send messages in the provided channel")))
} else {
ctx.JSON(500, utils.ErrorToResponse(err))
}
return
}
// add reactions to new message
if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil {
if err == request.ErrForbidden {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to add reactions in the provided channel")))
} else {
ctx.JSON(500, utils.ErrorToResponse(err))
}
return
}
// update DB
updated := database.MultiPanel{
Id: multiPanel.Id,
MessageId: messageId,
ChannelId: data.ChannelId,
GuildId: guildId,
Title: data.Title,
Content: data.Content,
Colour: int(data.Colour),
}
if err = dbclient.Client.MultiPanels.Update(multiPanel.Id, updated); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
// TODO: one query for ACID purposes
// delete old targets
if err := dbclient.Client.MultiPanelTargets.DeleteAll(multiPanel.Id); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
// insert new targets
group, _ := errgroup.WithContext(context.Background())
for _, panel := range panels {
panel := panel
group.Go(func() error {
return dbclient.Client.MultiPanelTargets.Insert(multiPanel.Id, panel.MessageId)
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
ctx.JSON(200, gin.H{
"success": true,
"data": multiPanel,
})
}

View File

@ -1,6 +1,7 @@
package api
import (
"errors"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
@ -64,6 +65,7 @@ func CreatePanel(ctx *gin.Context) {
}
if !data.doValidations(ctx, guildId) {
ctx.JSON(400, utils.ErrorToResponse(errors.New("Validation failed")))
return
}

View File

@ -1,15 +1,20 @@
package api
import (
"context"
"errors"
"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/database"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request"
"golang.org/x/sync/errgroup"
"strconv"
"sync"
)
func UpdatePanel(ctx *gin.Context) {
@ -17,29 +22,20 @@ func UpdatePanel(ctx *gin.Context) {
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
var data panel
if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err))
return
}
messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err))
return
}
@ -48,10 +44,7 @@ func UpdatePanel(ctx *gin.Context) {
// get existing
existing, err := dbclient.Client.Panel.Get(data.MessageId)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
@ -65,6 +58,59 @@ func UpdatePanel(ctx *gin.Context) {
}
if !data.doValidations(ctx, guildId) {
ctx.JSON(400, utils.ErrorToResponse(errors.New("Validation failed")))
return
}
// check if this will break a multi-panel;
// first, get any multipanels this panel belongs to
multiPanels, err := dbclient.Client.MultiPanelTargets.GetMultiPanels(existing.MessageId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
var wouldHaveDuplicateEmote bool
{
var duplicateLock sync.Mutex
group, _ := errgroup.WithContext(context.Background())
for _, multiPanelId := range multiPanels {
multiPanelId := multiPanelId
group.Go(func() error {
// get the sub-panels of the multi-panel
subPanels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanelId)
if err != nil {
return err
}
for _, subPanel := range subPanels {
if subPanel.MessageId == existing.MessageId {
continue
}
if subPanel.ReactionEmote == data.Emote {
duplicateLock.Lock()
wouldHaveDuplicateEmote = true
duplicateLock.Unlock()
break
}
}
return nil
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
}
if wouldHaveDuplicateEmote {
ctx.JSON(400, utils.ErrorToResponse(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel")))
return
}
@ -98,10 +144,7 @@ func UpdatePanel(ctx *gin.Context) {
})
} else {
// TODO: Most appropriate error?
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
}
return
@ -116,10 +159,7 @@ func UpdatePanel(ctx *gin.Context) {
})
} else {
// TODO: Most appropriate error?
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
}
return
@ -140,29 +180,20 @@ func UpdatePanel(ctx *gin.Context) {
}
if err = dbclient.Client.Panel.Update(messageId, panel); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
// insert role mention data
// delete old data
if err = dbclient.Client.PanelRoleMentions.DeleteAll(newMessageId); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
// TODO: Reduce to 1 query
if err = dbclient.Client.PanelUserMention.Set(newMessageId, false); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
@ -170,19 +201,13 @@ func UpdatePanel(ctx *gin.Context) {
for _, mention := range data.Mentions {
if mention == "user" {
if err = dbclient.Client.PanelUserMention.Set(newMessageId, true); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
} else {
roleId, err := strconv.ParseUint(mention, 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
@ -190,10 +215,7 @@ func UpdatePanel(ctx *gin.Context) {
// not too much of an issue if it isnt
if err = dbclient.Client.PanelRoleMentions.Add(newMessageId, roleId); err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err))
return
}
}

View File

@ -3,6 +3,14 @@ package http
import (
"fmt"
"github.com/TicketsBot/GoPanel/app/http/endpoints/api"
api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose"
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
api_logs "github.com/TicketsBot/GoPanel/app/http/endpoints/api/logs"
api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags"
api_ticket "github.com/TicketsBot/GoPanel/app/http/endpoints/api/ticket"
api_whitelabel "github.com/TicketsBot/GoPanel/app/http/endpoints/api/whitelabel"
"github.com/TicketsBot/GoPanel/app/http/endpoints/manage"
"github.com/TicketsBot/GoPanel/app/http/endpoints/root"
"github.com/TicketsBot/GoPanel/app/http/middleware"
@ -83,35 +91,40 @@ func StartServer() {
guildAuthApiSupport.GET("/user/:user", api.UserHandler)
guildAuthApiSupport.GET("/roles", api.RolesHandler)
guildAuthApiAdmin.GET("/settings", api.GetSettingsHandler)
guildAuthApiAdmin.POST("/settings", api.UpdateSettingsHandler)
guildAuthApiAdmin.GET("/settings", api_settings.GetSettingsHandler)
guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler)
guildAuthApiSupport.GET("/blacklist", api.GetBlacklistHandler)
guildAuthApiSupport.PUT("/blacklist", api.AddBlacklistHandler)
guildAuthApiSupport.DELETE("/blacklist/:user", api.RemoveBlacklistHandler)
guildAuthApiSupport.GET("/blacklist", api_blacklist.GetBlacklistHandler)
guildAuthApiSupport.PUT("/blacklist", api_blacklist.AddBlacklistHandler)
guildAuthApiSupport.DELETE("/blacklist/:user", api_blacklist.RemoveBlacklistHandler)
guildAuthApiAdmin.GET("/panels", api.ListPanels)
guildAuthApiAdmin.PUT("/panels", api.CreatePanel)
guildAuthApiAdmin.PUT("/panels/:message", api.UpdatePanel)
guildAuthApiAdmin.DELETE("/panels/:message", api.DeletePanel)
guildAuthApiAdmin.GET("/panels", api_panels.ListPanels)
guildAuthApiAdmin.PUT("/panels", api_panels.CreatePanel)
guildAuthApiAdmin.PUT("/panels/:message", api_panels.UpdatePanel)
guildAuthApiAdmin.DELETE("/panels/:message", api_panels.DeletePanel)
guildAuthApiSupport.GET("/logs/", api.GetLogs)
guildAuthApiSupport.GET("/modmail/logs/", api.GetModmailLogs)
guildAuthApiAdmin.GET("/multipanels", api_panels.MultiPanelList)
guildAuthApiAdmin.POST("/multipanels", api_panels.MultiPanelCreate)
guildAuthApiAdmin.PATCH("/multipanels/:panelid", api_panels.MultiPanelUpdate)
guildAuthApiAdmin.DELETE("/multipanels/:panelid", api_panels.MultiPanelDelete)
guildAuthApiSupport.GET("/tickets", api.GetTickets)
guildAuthApiSupport.GET("/tickets/:ticketId", api.GetTicket)
guildAuthApiSupport.POST("/tickets/:ticketId", api.SendMessage)
guildAuthApiSupport.DELETE("/tickets/:ticketId", api.CloseTicket)
guildAuthApiSupport.GET("/logs/", api_logs.GetLogs)
guildAuthApiSupport.GET("/modmail/logs/", api_logs.GetModmailLogs)
guildAuthApiSupport.GET("/tags", api.TagsListHandler)
guildAuthApiSupport.PUT("/tags", api.CreateTag)
guildAuthApiSupport.DELETE("/tags/:tag", api.DeleteTag)
guildAuthApiSupport.GET("/tickets", api_ticket.GetTickets)
guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket)
guildAuthApiSupport.POST("/tickets/:ticketId", api_ticket.SendMessage)
guildAuthApiSupport.DELETE("/tickets/:ticketId", api_ticket.CloseTicket)
guildAuthApiAdmin.GET("/claimsettings", api.GetClaimSettings)
guildAuthApiAdmin.POST("/claimsettings", api.PostClaimSettings)
guildAuthApiSupport.GET("/tags", api_tags.TagsListHandler)
guildAuthApiSupport.PUT("/tags", api_tags.CreateTag)
guildAuthApiSupport.DELETE("/tags/:tag", api_tags.DeleteTag)
guildAuthApiAdmin.GET("/autoclose", api.GetAutoClose)
guildAuthApiAdmin.POST("/autoclose", api.PostAutoClose)
guildAuthApiAdmin.GET("/claimsettings", api_settings.GetClaimSettings)
guildAuthApiAdmin.POST("/claimsettings", api_settings.PostClaimSettings)
guildAuthApiAdmin.GET("/autoclose", api_autoclose.GetAutoClose)
guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose)
}
userGroup := router.Group("/user", middleware.AuthenticateToken)
@ -123,13 +136,13 @@ func StartServer() {
whitelabelGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(false))
whitelabelApiGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(true))
whitelabelGroup.GET("/", api.WhitelabelGet)
whitelabelApiGroup.GET("/errors", api.WhitelabelGetErrors)
whitelabelApiGroup.GET("/guilds", api.WhitelabelGetGuilds)
whitelabelApiGroup.POST("/modmail", api.WhitelabelModmailPost)
whitelabelGroup.GET("/", api_whitelabel.WhitelabelGet)
whitelabelApiGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors)
whitelabelApiGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds)
whitelabelApiGroup.POST("/modmail", api_whitelabel.WhitelabelModmailPost)
whitelabelApiGroup.Group("/").Use(createLimiter(10, time.Minute)).POST("/", api.WhitelabelPost)
whitelabelApiGroup.Group("/").Use(createLimiter(1, time.Second * 5)).POST("/status", api.WhitelabelStatusPost)
whitelabelApiGroup.Group("/").Use(createLimiter(10, time.Minute)).POST("/", api_whitelabel.WhitelabelPost)
whitelabelApiGroup.Group("/").Use(createLimiter(1, time.Second * 5)).POST("/status", api_whitelabel.WhitelabelStatusPost)
}
}
@ -150,7 +163,7 @@ func createRenderer() multitemplate.Renderer {
r = addManageTemplate(r, "settings", "./public/templates/includes/substitutionmodal.tmpl")
r = addManageTemplate(r, "ticketlist")
r = addManageTemplate(r, "ticketview")
r = addManageTemplate(r, "panels", "./public/templates/includes/substitutionmodal.tmpl", "./public/templates/includes/paneleditmodal.tmpl")
r = addManageTemplate(r, "panels", "./public/templates/includes/substitutionmodal.tmpl", "./public/templates/includes/paneleditmodal.tmpl", "./public/templates/includes/multipaneleditmodal.tmpl")
r = addManageTemplate(r, "tags")
r = addErrorTemplate(r)
@ -180,6 +193,7 @@ func addManageTemplate(renderer multitemplate.Renderer, name string, extra ...st
"./public/templates/includes/sidebar.tmpl",
"./public/templates/includes/navbar.tmpl",
"./public/templates/includes/loadingscreen.tmpl",
"./public/templates/includes/notifymodal.tmpl",
fmt.Sprintf("./public/templates/views/%s.tmpl", name),
}

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20200704164621-09d42dd941e0
github.com/TicketsBot/common v0.0.0-20200702195837-7afe5e77d1df
github.com/TicketsBot/database v0.0.0-20200708121851-08f2e7582b28
github.com/TicketsBot/database v0.0.0-20200723134637-72f4cd31eef6
github.com/TicketsBot/logarchiver v0.0.0-20200425163447-199b93429026 // indirect
github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect

View File

@ -212,7 +212,7 @@ html > ::-webkit-scrollbar {
background-color: #121212;
height: 100px;
margin-bottom: 10px;
border-radius: 25px;
border-radius: 10px;
display: flex;
cursor: pointer;
}

View File

@ -0,0 +1,147 @@
{{define "multipaneleditmodal"}}
<div class="modal fade" id="multieditmodal" tabindex="-1" role="dialog" aria-labelledby="multieditmodal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><b>Edit Multi-Panel</b></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<input type="hidden" id="multi-edit-id">
<div class="row">
<div class="col-md-4 pr-1">
<div class="form-group">
<label class="black">Embed Title</label>
<input type="text" class="form-control" placeholder="React to open a ticket" id="multi-edit-title">
</div>
</div>
<div class="col-md-8 pr-1">
<div class="form-group">
<label class="black">Embed Content</label>
<textarea type="text" class="form-control"
placeholder="Let users know which reaction corresponds to which panel. You are able to use emojis here."
id="multi-edit-content"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-2 pr-1">
<label class="black">Embed Colour</label>
<div class="input-group mb-3">
<input type="color" class="form-control input-fill" id="multi-edit-colour">
</div>
</div>
<div class="col-md-4 pr-1">
<label class="black">Embed Channel</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">#</div>
</div>
<select class="form-control" id="multi-edit-channel-container">
</select>
</div>
</div>
<div class="col-md-6 pr-1">
<div class="form-group">
<label class="black" for="mentions">Panels</label>
<select class="selectpicker form-control" id="multi-edit-panels" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static">
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-fill" onclick="updateMultiPanel()">Save</button>
</div>
</div>
</div>
</div>
<script>
function resetMultiEditModal() {
clear('multi-edit-title', 'multi-edit-content', 'multi-edit-colour');
$('#multi-edit-panels').selectpicker('deselectAll');
}
registerHideListener('multieditmodal');
$('#multieditmodal').on('hidden.bs.modal', resetEditModal);
async function openMultiEditModal(id) {
resetMultiEditModal();
const res = await axios.get('/api/{{.guildId}}/multipanels');
if (res.status !== 200) {
showToast("Error", res.data);
return;
}
const panel = res.data.data.find(panel => panel.id === id);
if (panel === undefined) {
showToast('Error', 'Panel not found');
return;
}
await fillMultiEditData(panel);
$('#multieditmodal').modal('show');
showBackdrop();
}
async function fillMultiEditData(panel) {
document.getElementById('multi-edit-id').value = panel.id;
document.getElementById('multi-edit-title').value = panel.title;
document.getElementById('multi-edit-content').value = panel.content;
document.getElementById('multi-edit-colour').value = `#${panel.colour.toString(16)}`;
const channels = await getChannels();
fillChannels('multi-edit-channel-container', channels);
setActiveChannel('multi-edit-channel-container', panel.channel_id);
// fill panel dropdown
const res = await axios.get('/api/{{.guildId}}/panels');
if (res.status !== 200) {
showToast("Error", res.data);
return 0;
}
$('#multi-edit-panels').selectpicker('val', panel.panels.map(p => p.message_id));
}
async function updateMultiPanel() {
const channelContainer = document.getElementById('multi-edit-channel-container');
const panelId = getValue('multi-edit-id');
const data = {
'title': getValue('multi-edit-title'),
'content': getValue('multi-edit-content'),
'colour': parseInt(`0x${getValue('multi-edit-colour').slice(1)}`),
'channel_id': channelContainer.options[channelContainer.selectedIndex].value,
'panels': $('#multi-edit-panels').val()
};
$('#multieditmodal').modal('hide');
const res = await axios.patch('/api/{{.guildId}}/multipanels/' + panelId, data);
if (res.status !== 200 || !res.data.success) {
notifyError(res.data.error);
return;
}
// update table
const tr = document.getElementById(panelId);
tr.children[0].textContent = data.title;
notify('Success', 'Multi-panel updated');
}
</script>
{{end}}

View File

@ -0,0 +1,52 @@
{{define "notifymodal"}}
<div class="modal fade" id="notificationmodal" tabindex="-1" role="dialog" aria-labelledby="notificationmodal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><b id="notification-title"></b></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="col-md-10 offset-md-1">
<p id="notification-message" style="text-align: center"></p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-fill" onclick="closeNotificationModal()">Dismiss</button>
</div>
</div>
</div>
</div>
<script>
registerHideListener('notificationmodal');
function notify(title, message) {
document.getElementById('notification-title').textContent = title;
document.getElementById('notification-message').textContent = message;
$('#notificationmodal').modal('show');
showBackdrop();
}
function notifyError(message) {
notify('Error', message);
}
function notifySuccess(message) {
notify('Success', message);
}
function closeNotificationModal() {
$('#notificationmodal').modal('hide');
}
</script>
{{end}}

View File

@ -147,25 +147,15 @@
}
const channels = await getChannels();
await fillChannels('edit-channel-container', channels);
await fillCategories('edit-category-container', channels);
fillChannels('edit-channel-container', channels);
fillCategories('edit-category-container', channels);
await fillMentions('edit-mentions');
setActiveChannel(panel);
setActiveChannel('edit-channel-container', panel.channel_id);
setActiveCategory(panel);
setActiveMentions(panel);
}
function setActiveChannel(panel) {
const select = document.getElementById('edit-channel-container');
for (let i = 0; i < select.children.length; i++) {
const child = select.children[i];
if (child.value === panel.channel_id) {
select.selectedIndex = i;
}
}
}
function setActiveCategory(panel) {
const select = document.getElementById('edit-category-container');
for (let i = 0; i < select.children.length; i++) {

View File

@ -8,6 +8,10 @@
<div class="main-panel" style="width: 100% !important;">
{{template "navbar" .}}
{{template "loadingscreen" .}}
<script src="/assets/js/modalbackdrop.js"></script>
{{template "notifymodal" .}}
{{template "content" .}}
</div>
</div>

View File

@ -1,12 +1,12 @@
{{define "content"}}
<script src="/assets/js/modalbackdrop.js"></script>
{{template "substitutions" .}}
{{template "paneleditmodal" .}}
{{template "multipaneleditmodal" .}}
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">Reaction Panels</h4>
@ -31,6 +31,30 @@
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4 class="card-title">Multi-Panels</h4>
</div>
<div class="card-body">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Embed Title</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="multi-panel-container">
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">Create A Panel</h4>
@ -133,15 +157,83 @@
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h4 class="card-title">Create A Multi-Panel</h4>
</div>
<div class="card-body">
<form onsubmit="createMultiPanel(); return false;">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="black">Embed Title</label>
<input type="text" class="form-control" placeholder="React to open a ticket" id="multi-title">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="black">Embed Content</label>
<textarea type="text" class="form-control"
placeholder="Let users know which reaction corresponds to which panel. You are able to use emojis here." id="multi-content"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<label class="black">Embed Colour</label>
<div class="input-group mb-3">
<input name="colour" type="color" class="form-control input-fill" value="#7289da" id="multi-colour">
</div>
</div>
<div class="col-md-9">
<label class="black">Embed Channel</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">#</div>
</div>
<select class="form-control" id="multi-channel-container">
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label class="black" for="mentions">Panels</label>
<select class="selectpicker form-control" id="multi-panels" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static">
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 pr-1 offset-md-3">
<div class="form-group text-center">
<button type="submit" class="btn btn-primary btn-fill" style="width: 100%;"><i class="fas fa-paper-plane"></i> Submit</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;">
<div aria-live="polite" aria-atomic="true" style="position: relative">
<div style="position: absolute; right: 10px" id="toast-container">
</div>
</div>
<script>
function getValue(elementId) {
return document.getElementById(elementId).value;
}
async function getChannels() {
const res = await axios.get('/api/{{.guildId}}/channels');
return res.data;
@ -169,6 +261,19 @@
}
}
async function deleteMultiPanel(panelId) {
const res = await axios.delete('/api/{{.guildId}}/multipanels/' + panelId);
if (res.status === 200 && res.data.success) {
notifySuccess('Multi-panel deleted successfully');
const el = document.getElementById(panelId);
el.parentNode.removeChild(el);
} else {
notifyError(res.data.error);
}
}
async function createPanel() {
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
@ -196,7 +301,28 @@
}
}
async function fillChannels(elementId, channels) {
async function createMultiPanel() {
const channelContainer = document.getElementById('multi-channel-container');
const data = {
'title': getValue('multi-title'),
'content': getValue('multi-content'),
'colour': parseInt(`0x${getValue('multi-colour').slice(1)}`),
'channel_id': channelContainer.options[channelContainer.selectedIndex].value,
'panels': $('#multi-panels').val()
};
const res = await axios.post('/api/{{.guildId}}/multipanels', data);
if (res.status !== 200 || !res.data.success) {
notifyError(res.data.error);
return;
}
appendMultiPanel(res.data.data);
notify('Success', 'Multi-panel created successfully. Note: Don\'t delete the existing panels, or your they will disappear from your multi-panel.');
}
function fillChannels(elementId, channels) {
const container = document.getElementById(elementId);
channels.filter(ch => ch.type === 0).forEach(ch => {
@ -207,7 +333,7 @@
});
}
async function fillCategories(elementId, channels) {
function fillCategories(elementId, channels) {
const container = document.getElementById(elementId);
channels.filter(ch => ch.type === 4).forEach(ch => {
@ -218,6 +344,16 @@
});
}
function setActiveChannel(elementId, channelId) {
const select = document.getElementById(elementId);
for (let i = 0; i < select.children.length; i++) {
const child = select.children[i];
if (child.value === channelId) {
select.selectedIndex = i;
}
}
}
// TODO: Update on append / delete
async function fillPanelQuota(panelCount) {
const res = await axios.get('/api/{{.guildId}}/premium');
@ -267,6 +403,37 @@
container.appendChild(tr);
}
function appendMultiPanel(panel) {
const container = document.getElementById('multi-panel-container');
const tr = document.createElement('tr');
tr.id = panel.id;
appendTd(tr, panel.title);
// build edit button
const editTd = document.createElement('td');
const editButton = document.createElement('button');
editButton.type = 'button';
editButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto');
editButton.appendChild(document.createTextNode('Edit'));
editButton.onclick = () => { openMultiEditModal(panel.id) };
editTd.appendChild(editButton);
tr.appendChild(editTd);
// build remove button
const deleteTd = document.createElement('td');
const deleteButton = document.createElement('button');
deleteButton.type = 'submit';
deleteButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto');
deleteButton.appendChild(document.createTextNode('Delete'));
deleteButton.onclick = () => {deleteMultiPanel(panel.id)};
deleteTd.appendChild(deleteButton);
tr.appendChild(deleteTd);
container.appendChild(tr);
}
async function fillPanels(channels) {
const res = await axios.get('/api/{{.guildId}}/panels');
if (res.status !== 200) {
@ -278,9 +445,37 @@
appendPanel(panel, channels);
}
appendPanelDropdownPanels('multi-panels', res.data);
appendPanelDropdownPanels('multi-edit-panels', res.data);
return res.data.length;
}
async function fillMultiPanels() {
const res = await axios.get('/api/{{.guildId}}/multipanels');
if (res.status !== 200) {
showToast("Error", res.data);
return;
}
for (const multiPanel of res.data.data) {
appendMultiPanel(multiPanel);
}
}
function appendPanelDropdownPanels(elementId, panels) {
const select = document.getElementById(elementId);
for (const panel of panels) {
const option = document.createElement('option');
option.value = panel.message_id;
option.appendChild(document.createTextNode(panel.title));
select.appendChild(option);
}
$(`#${elementId}`).selectpicker('refresh');
}
async function fillMentions(elementId) {
const select = document.getElementById(elementId);
@ -313,9 +508,12 @@
const channels = await getChannels();
const panelCount = await fillPanels(channels);
await fillMultiPanels();
await fillPanelQuota(panelCount);
await fillChannels('channel-container', channels);
await fillCategories('category-container', channels);
fillChannels('channel-container', channels);
fillChannels('multi-channel-container', channels);
fillCategories('category-container', channels);
await fillMentions('mentions');
}

View File

@ -80,7 +80,7 @@
document.getElementById('id').value = '';
document.getElementById('content').value = '';
appendTag(data);
appendTag(data.id, data.content);
} else {
showToast('Error', res.data.error);
}

14
utils/requestutils.go Normal file
View File

@ -0,0 +1,14 @@
package utils
import "github.com/gin-gonic/gin"
func ErrorToResponse(err error) map[string]interface{} {
return gin.H {
"success": false,
"error": err.Error(),
}
}
var SuccessResponse = gin.H{
"success": true,
}