Support teams

This commit is contained in:
rxdn 2021-02-18 16:31:23 +00:00
parent d6951e89ad
commit 741e041190
32 changed files with 1312 additions and 139 deletions

View File

@ -46,7 +46,7 @@ func GetPermissionLevel(ctx *gin.Context) {
} }
if err := group.Wait(); err != nil { if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -33,14 +33,14 @@ func MultiPanelCreate(ctx *gin.Context) {
var data multiPanelCreateData var data multiPanelCreateData
if err := ctx.ShouldBindJSON(&data); err != nil { if err := ctx.ShouldBindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
// validate body & get sub-panels // validate body & get sub-panels
panels, err := data.doValidations(guildId) panels, err := data.doValidations(guildId)
if err != nil { if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
@ -61,9 +61,9 @@ func MultiPanelCreate(ctx *gin.Context) {
if err != nil { if err != nil {
var unwrapped request.RestError var unwrapped request.RestError
if errors.As(err, &unwrapped); unwrapped.ErrorCode == 403 { if errors.As(err, &unwrapped); unwrapped.ErrorCode == 403 {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to send messages in the provided channel"))) ctx.JSON(500, utils.ErrorJson(errors.New("I do not have permission to send messages in the provided channel")))
} else { } else {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
} }
return return
@ -72,9 +72,9 @@ func MultiPanelCreate(ctx *gin.Context) {
if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil { if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil {
var unwrapped request.RestError var unwrapped request.RestError
if errors.As(err, &unwrapped); unwrapped.ErrorCode == 403{ if errors.As(err, &unwrapped); unwrapped.ErrorCode == 403{
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to add reactions in the provided channel"))) ctx.JSON(500, utils.ErrorJson(errors.New("I do not have permission to add reactions in the provided channel")))
} else { } else {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
} }
return return
@ -91,7 +91,7 @@ func MultiPanelCreate(ctx *gin.Context) {
multiPanel.Id, err = dbclient.Client.MultiPanels.Create(multiPanel) multiPanel.Id, err = dbclient.Client.MultiPanels.Create(multiPanel)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -105,7 +105,7 @@ func MultiPanelCreate(ctx *gin.Context) {
} }
if err := group.Wait(); err != nil { if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -16,42 +16,42 @@ func MultiPanelDelete(ctx *gin.Context) {
multiPanelId, err := strconv.Atoi(ctx.Param("panelid")) multiPanelId, err := strconv.Atoi(ctx.Param("panelid"))
if err != nil { if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
// get bot context // get bot context
botContext, err := botcontext.ContextForGuild(guildId) botContext, err := botcontext.ContextForGuild(guildId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
panel, ok, err := dbclient.Client.MultiPanels.Get(multiPanelId) panel, ok, err := dbclient.Client.MultiPanels.Get(multiPanelId)
if !ok { if !ok {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with matching ID found"))) ctx.JSON(404, utils.ErrorJson(errors.New("No panel with matching ID found")))
return return
} }
if panel.GuildId != guildId { if panel.GuildId != guildId {
ctx.JSON(403, utils.ErrorToResponse(errors.New("Guild ID doesn't match"))) ctx.JSON(403, utils.ErrorJson(errors.New("Guild ID doesn't match")))
return return
} }
var unwrapped request.RestError var unwrapped request.RestError
if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, panel.ChannelId, panel.MessageId); err != nil && !(errors.As(err, &unwrapped) && unwrapped.IsClientError()) { if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, panel.ChannelId, panel.MessageId); err != nil && !(errors.As(err, &unwrapped) && unwrapped.IsClientError()) {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
success, err := dbclient.Client.MultiPanels.Delete(guildId, multiPanelId) success, err := dbclient.Client.MultiPanels.Delete(guildId, multiPanelId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
if !success { if !success {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with matching ID found"))) ctx.JSON(404, utils.ErrorJson(errors.New("No panel with matching ID found")))
return return
} }

View File

@ -19,7 +19,7 @@ func MultiPanelList(ctx *gin.Context) {
multiPanels, err := dbclient.Client.MultiPanels.GetByGuild(guildId) multiPanels, err := dbclient.Client.MultiPanels.GetByGuild(guildId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -45,7 +45,7 @@ func MultiPanelList(ctx *gin.Context) {
} }
if err := group.Wait(); err != nil { if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -22,54 +22,54 @@ func MultiPanelUpdate(ctx *gin.Context) {
// parse body // parse body
var data multiPanelCreateData var data multiPanelCreateData
if err := ctx.ShouldBindJSON(&data); err != nil { if err := ctx.ShouldBindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
// parse panel ID // parse panel ID
panelId, err := strconv.Atoi(ctx.Param("panelid")) panelId, err := strconv.Atoi(ctx.Param("panelid"))
if err != nil { if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
// retrieve panel from DB // retrieve panel from DB
multiPanel, ok, err := dbclient.Client.MultiPanels.Get(panelId) multiPanel, ok, err := dbclient.Client.MultiPanels.Get(panelId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
// check panel exists // check panel exists
if !ok { if !ok {
ctx.JSON(404, utils.ErrorToResponse(errors.New("No panel with the provided ID found"))) ctx.JSON(404, utils.ErrorJson(errors.New("No panel with the provided ID found")))
return return
} }
// check panel is in the same guild // check panel is in the same guild
if guildId != multiPanel.GuildId { if guildId != multiPanel.GuildId {
ctx.JSON(403, utils.ErrorToResponse(errors.New("Guild ID doesn't match"))) ctx.JSON(403, utils.ErrorJson(errors.New("Guild ID doesn't match")))
return return
} }
// validate body & get sub-panels // validate body & get sub-panels
panels, err := data.doValidations(guildId) panels, err := data.doValidations(guildId)
if err != nil { if err != nil {
ctx.JSON(400, utils.ErrorToResponse(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
} }
// get bot context // get bot context
botContext, err := botcontext.ContextForGuild(guildId) botContext, err := botcontext.ContextForGuild(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
// delete old message // delete old message
var unwrapped request.RestError var unwrapped request.RestError
if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId); err != nil && !(errors.As(err, &unwrapped) && unwrapped.IsClientError()) { if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId); err != nil && !(errors.As(err, &unwrapped) && unwrapped.IsClientError()) {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -81,9 +81,9 @@ func MultiPanelUpdate(ctx *gin.Context) {
if err != nil { if err != nil {
var unwrapped request.RestError var unwrapped request.RestError
if errors.As(err, &unwrapped) && unwrapped.ErrorCode == 403 { if errors.As(err, &unwrapped) && unwrapped.ErrorCode == 403 {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to send messages in the provided channel"))) ctx.JSON(500, utils.ErrorJson(errors.New("I do not have permission to send messages in the provided channel")))
} else { } else {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
} }
return return
@ -93,9 +93,9 @@ func MultiPanelUpdate(ctx *gin.Context) {
if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil { if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil {
var unwrapped request.RestError var unwrapped request.RestError
if errors.As(err, &unwrapped) && unwrapped.ErrorCode == 403 { if errors.As(err, &unwrapped) && unwrapped.ErrorCode == 403 {
ctx.JSON(500, utils.ErrorToResponse(errors.New("I do not have permission to add reactions in the provided channel"))) ctx.JSON(500, utils.ErrorJson(errors.New("I do not have permission to add reactions in the provided channel")))
} else { } else {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
} }
return return
@ -113,14 +113,14 @@ func MultiPanelUpdate(ctx *gin.Context) {
} }
if err = dbclient.Client.MultiPanels.Update(multiPanel.Id, updated); err != nil { if err = dbclient.Client.MultiPanels.Update(multiPanel.Id, updated); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
// TODO: one query for ACID purposes // TODO: one query for ACID purposes
// delete old targets // delete old targets
if err := dbclient.Client.MultiPanelTargets.DeleteAll(multiPanel.Id); err != nil { if err := dbclient.Client.MultiPanelTargets.DeleteAll(multiPanel.Id); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -135,7 +135,7 @@ func MultiPanelUpdate(ctx *gin.Context) {
} }
if err := group.Wait(); err != nil { if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -1,7 +1,9 @@
package api package api
import ( import (
"context"
"errors" "errors"
"fmt"
"github.com/TicketsBot/GoPanel/botcontext" "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"
@ -15,13 +17,28 @@ import (
"github.com/rxdn/gdl/objects/channel/message" "github.com/rxdn/gdl/objects/channel/message"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request" "github.com/rxdn/gdl/rest/request"
"golang.org/x/sync/errgroup"
"strconv" "strconv"
"strings" "strings"
) )
const freePanelLimit = 3 const freePanelLimit = 3
type panelBody struct {
ChannelId uint64 `json:"channel_id,string"`
MessageId uint64 `json:"message_id,string"`
Title string `json:"title"`
Content string `json:"content"`
Colour uint32 `json:"colour"`
CategoryId uint64 `json:"category_id,string"`
Emote string `json:"emote"`
WelcomeMessage *string `json:"welcome_message"`
Mentions []string `json:"mentions"`
Teams []string `json:"teams"`
}
func CreatePanel(ctx *gin.Context) { func CreatePanel(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
botContext, err := botcontext.ContextForGuild(guildId) botContext, err := botcontext.ContextForGuild(guildId)
@ -33,7 +50,7 @@ func CreatePanel(ctx *gin.Context) {
return return
} }
var data panel var data panelBody
if err := ctx.BindJSON(&data); err != nil { if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.AbortWithStatusJSON(400, gin.H{
@ -120,6 +137,7 @@ func CreatePanel(ctx *gin.Context) {
TargetCategory: data.CategoryId, TargetCategory: data.CategoryId,
ReactionEmote: emoji, ReactionEmote: emoji,
WelcomeMessage: data.WelcomeMessage, WelcomeMessage: data.WelcomeMessage,
WithDefaultTeam: utils.ContainsString(data.Teams, "default"),
} }
if err = dbclient.Client.Panel.Create(panel); err != nil { if err = dbclient.Client.Panel.Create(panel); err != nil {
@ -164,13 +182,50 @@ func CreatePanel(ctx *gin.Context) {
} }
} }
if responseCode, err := insertTeams(guildId, msgId, data.Teams); err != nil {
ctx.JSON(responseCode, utils.ErrorJson(err))
return
}
ctx.JSON(200, gin.H{ ctx.JSON(200, gin.H{
"success": true, "success": true,
"message_id": strconv.FormatUint(msgId, 10), "message_id": strconv.FormatUint(msgId, 10),
}) })
} }
func (p *panel) doValidations(ctx *gin.Context, guildId uint64) bool { // returns (response_code, error)
func insertTeams(guildId, panelMessageId uint64, teamIds []string) (int, error) {
// insert teams
group, _ := errgroup.WithContext(context.Background())
for _, teamId := range teamIds {
if teamId == "default" {
continue // already handled
}
teamId, err := strconv.Atoi(teamId)
if err != nil {
return 400, err
}
group.Go(func() error {
// ensure team exists
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("team with id %d not found", teamId)
}
return dbclient.Client.PanelTeams.Add(panelMessageId, teamId)
})
}
return 500, group.Wait()
}
func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
if !p.verifyTitle() { if !p.verifyTitle() {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.AbortWithStatusJSON(400, gin.H{
"success": false, "success": false,
@ -225,22 +280,22 @@ func (p *panel) doValidations(ctx *gin.Context, guildId uint64) bool {
return true return true
} }
func (p *panel) verifyTitle() bool { func (p *panelBody) verifyTitle() bool {
return len(p.Title) > 0 && len(p.Title) < 256 return len(p.Title) > 0 && len(p.Title) < 256
} }
func (p *panel) verifyContent() bool { func (p *panelBody) verifyContent() bool {
return len(p.Content) > 0 && len(p.Content) < 1025 return len(p.Content) > 0 && len(p.Content) < 1025
} }
func (p *panel) getEmoji() (emoji string, ok bool) { func (p *panelBody) getEmoji() (emoji string, ok bool) {
p.Emote = strings.Replace(p.Emote, ":", "", -1) p.Emote = strings.Replace(p.Emote, ":", "", -1)
emoji, ok = utils.GetEmoji(p.Emote) emoji, ok = utils.GetEmoji(p.Emote)
return return
} }
func (p *panel) verifyChannel(channels []channel.Channel) bool { func (p *panelBody) verifyChannel(channels []channel.Channel) bool {
var valid bool var valid bool
for _, ch := range channels { for _, ch := range channels {
if ch.Id == p.ChannelId && ch.Type == channel.ChannelTypeGuildText { if ch.Id == p.ChannelId && ch.Type == channel.ChannelTypeGuildText {
@ -252,7 +307,7 @@ func (p *panel) verifyChannel(channels []channel.Channel) bool {
return valid return valid
} }
func (p *panel) verifyCategory(channels []channel.Channel) bool { func (p *panelBody) verifyCategory(channels []channel.Channel) bool {
var valid bool var valid bool
for _, ch := range channels { for _, ch := range channels {
if ch.Id == p.CategoryId && ch.Type == channel.ChannelTypeGuildCategory { if ch.Id == p.CategoryId && ch.Type == channel.ChannelTypeGuildCategory {
@ -264,11 +319,11 @@ func (p *panel) verifyCategory(channels []channel.Channel) bool {
return valid return valid
} }
func (p *panel) verifyWelcomeMessage() bool { func (p *panelBody) verifyWelcomeMessage() bool {
return p.WelcomeMessage == nil || (len(*p.WelcomeMessage) > 0 && len(*p.WelcomeMessage) < 1025) return p.WelcomeMessage == nil || (len(*p.WelcomeMessage) > 0 && len(*p.WelcomeMessage) < 1025)
} }
func (p *panel) sendEmbed(ctx *botcontext.BotContext, isPremium bool) (messageId uint64, err error) { func (p *panelBody) sendEmbed(ctx *botcontext.BotContext, isPremium bool) (messageId uint64, err error) {
e := embed.NewEmbed(). e := embed.NewEmbed().
SetTitle(p.Title). SetTitle(p.Title).
SetDescription(p.Content). SetDescription(p.Content).

View File

@ -2,13 +2,15 @@ package api
import ( import (
"context" "context"
"github.com/TicketsBot/GoPanel/database" dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"strconv" "strconv"
) )
type panel struct { func ListPanels(ctx *gin.Context) {
type panelResponse struct {
ChannelId uint64 `json:"channel_id,string"` ChannelId uint64 `json:"channel_id,string"`
MessageId uint64 `json:"message_id,string"` MessageId uint64 `json:"message_id,string"`
Title string `json:"title"` Title string `json:"title"`
@ -18,12 +20,13 @@ type panel struct {
Emote string `json:"emote"` Emote string `json:"emote"`
WelcomeMessage *string `json:"welcome_message"` WelcomeMessage *string `json:"welcome_message"`
Mentions []string `json:"mentions"` Mentions []string `json:"mentions"`
WithDefaultTeam bool `json:"default_team"`
Teams []database.SupportTeam `json:"teams"`
} }
func ListPanels(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
panels, err := database.Client.Panel.GetByGuild(guildId) panels, err := dbclient.Client.Panel.GetByGuild(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{ ctx.AbortWithStatusJSON(500, gin.H{
"success": false, "success": false,
@ -32,7 +35,7 @@ func ListPanels(ctx *gin.Context) {
return return
} }
wrapped := make([]panel, len(panels)) wrapped := make([]panelResponse, len(panels))
// we will need to lookup role mentions // we will need to lookup role mentions
group, _ := errgroup.WithContext(context.Background()) group, _ := errgroup.WithContext(context.Background())
@ -45,7 +48,7 @@ func ListPanels(ctx *gin.Context) {
var mentions []string var mentions []string
// get role mentions // get role mentions
roles, err := database.Client.PanelRoleMentions.GetRoles(p.MessageId) roles, err := dbclient.Client.PanelRoleMentions.GetRoles(p.MessageId)
if err != nil { if err != nil {
return err return err
} }
@ -56,7 +59,7 @@ func ListPanels(ctx *gin.Context) {
} }
// get if we should mention the ticket opener // get if we should mention the ticket opener
shouldMention, err := database.Client.PanelUserMention.ShouldMentionUser(p.MessageId) shouldMention, err := dbclient.Client.PanelUserMention.ShouldMentionUser(p.MessageId)
if err != nil { if err != nil {
return err return err
} }
@ -65,7 +68,12 @@ func ListPanels(ctx *gin.Context) {
mentions = append(mentions, "user") mentions = append(mentions, "user")
} }
wrapped[i] = panel{ teams, err := dbclient.Client.PanelTeams.GetTeams(p.MessageId)
if err != nil {
return err
}
wrapped[i] = panelResponse{
MessageId: p.MessageId, MessageId: p.MessageId,
ChannelId: p.ChannelId, ChannelId: p.ChannelId,
Title: p.Title, Title: p.Title,
@ -75,6 +83,8 @@ func ListPanels(ctx *gin.Context) {
Emote: p.ReactionEmote, Emote: p.ReactionEmote,
WelcomeMessage: p.WelcomeMessage, WelcomeMessage: p.WelcomeMessage,
Mentions: mentions, Mentions: mentions,
WithDefaultTeam: p.WithDefaultTeam,
Teams: teams,
} }
return nil return nil

View File

@ -22,20 +22,20 @@ func UpdatePanel(ctx *gin.Context) {
botContext, err := botcontext.ContextForGuild(guildId) botContext, err := botcontext.ContextForGuild(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
var data panel var data panelBody
if err := ctx.BindJSON(&data); err != nil { if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(400, utils.ErrorJson(err))
return return
} }
messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64) messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(400, utils.ErrorJson(err))
return return
} }
@ -44,7 +44,7 @@ func UpdatePanel(ctx *gin.Context) {
// get existing // get existing
existing, err := dbclient.Client.Panel.Get(data.MessageId) existing, err := dbclient.Client.Panel.Get(data.MessageId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
@ -65,7 +65,7 @@ func UpdatePanel(ctx *gin.Context) {
// first, get any multipanels this panel belongs to // first, get any multipanels this panel belongs to
multiPanels, err := dbclient.Client.MultiPanelTargets.GetMultiPanels(existing.MessageId) multiPanels, err := dbclient.Client.MultiPanelTargets.GetMultiPanels(existing.MessageId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -103,13 +103,13 @@ func UpdatePanel(ctx *gin.Context) {
} }
if err := group.Wait(); err != nil { if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
} }
if wouldHaveDuplicateEmote { if wouldHaveDuplicateEmote {
ctx.JSON(400, utils.ErrorToResponse(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel"))) ctx.JSON(400, utils.ErrorJson(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel")))
return return
} }
@ -144,7 +144,7 @@ func UpdatePanel(ctx *gin.Context) {
}) })
} else { } else {
// TODO: Most appropriate error? // TODO: Most appropriate error?
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
} }
return return
@ -160,7 +160,7 @@ func UpdatePanel(ctx *gin.Context) {
}) })
} else { } else {
// TODO: Most appropriate error? // TODO: Most appropriate error?
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
} }
return return
@ -178,23 +178,24 @@ func UpdatePanel(ctx *gin.Context) {
TargetCategory: data.CategoryId, TargetCategory: data.CategoryId,
ReactionEmote: emoji, ReactionEmote: emoji,
WelcomeMessage: data.WelcomeMessage, WelcomeMessage: data.WelcomeMessage,
WithDefaultTeam: utils.ContainsString(data.Teams, "default"),
} }
if err = dbclient.Client.Panel.Update(messageId, panel); err != nil { if err = dbclient.Client.Panel.Update(messageId, panel); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
// insert role mention data // insert role mention data
// delete old data // delete old data
if err = dbclient.Client.PanelRoleMentions.DeleteAll(newMessageId); err != nil { if err = dbclient.Client.PanelRoleMentions.DeleteAll(newMessageId); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
// TODO: Reduce to 1 query // TODO: Reduce to 1 query
if err = dbclient.Client.PanelUserMention.Set(newMessageId, false); err != nil { if err = dbclient.Client.PanelUserMention.Set(newMessageId, false); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
@ -202,13 +203,13 @@ func UpdatePanel(ctx *gin.Context) {
for _, mention := range data.Mentions { for _, mention := range data.Mentions {
if mention == "user" { if mention == "user" {
if err = dbclient.Client.PanelUserMention.Set(newMessageId, true); err != nil { if err = dbclient.Client.PanelUserMention.Set(newMessageId, true); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
} else { } else {
roleId, err := strconv.ParseUint(mention, 10, 64) roleId, err := strconv.ParseUint(mention, 10, 64)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
@ -216,12 +217,26 @@ func UpdatePanel(ctx *gin.Context) {
// not too much of an issue if it isnt // not too much of an issue if it isnt
if err = dbclient.Client.PanelRoleMentions.Add(newMessageId, roleId); err != nil { if err = dbclient.Client.PanelRoleMentions.Add(newMessageId, roleId); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }
} }
} }
// insert support teams
// TODO: Stop race conditions - 1 transaction
// delete teams
if err := dbclient.Client.PanelTeams.DeleteAll(newMessageId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// insert new
if responseCode, err := insertTeams(guildId, newMessageId, data.Teams); err != nil {
ctx.JSON(responseCode, utils.ErrorJson(err))
return
}
ctx.JSON(200, gin.H{ ctx.JSON(200, gin.H{
"success": true, "success": true,
"message_id": strconv.FormatUint(newMessageId, 10), "message_id": strconv.FormatUint(newMessageId, 10),

View File

@ -16,14 +16,14 @@ func ReloadGuildsHandler(ctx *gin.Context) {
key := fmt.Sprintf("tickets:dashboard:guildreload:%d", userId) key := fmt.Sprintf("tickets:dashboard:guildreload:%d", userId)
res, err := messagequeue.Client.SetNX(key, 1, time.Second*10).Result() res, err := messagequeue.Client.SetNX(key, 1, time.Second*10).Result()
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
if !res { if !res {
ttl, err := messagequeue.Client.TTL(key).Result() ttl, err := messagequeue.Client.TTL(key).Result()
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
@ -66,7 +66,7 @@ func ReloadGuildsHandler(ctx *gin.Context) {
} }
if err := utils.LoadGuilds(accessToken, userId); err != nil { if err := utils.LoadGuilds(accessToken, userId); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -0,0 +1,31 @@
package api
import (
"github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
)
func SearchMembers(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
botCtx, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
query := ctx.Query("query")
if len(query) == 0 || len(query) > 32 {
ctx.JSON(400, utils.ErrorStr("Invalid query"))
return
}
members, err := botCtx.SearchMembers(guildId, query)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, members)
}

View File

@ -0,0 +1,88 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func AddMember(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
snowflake, err := strconv.ParseUint(ctx.Param("snowflake"), 10, 64)
if err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
// get entity type
typeParsed, err := strconv.Atoi(ctx.Query("type"))
if err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
entityType, ok := entityTypes[typeParsed]
if !ok {
ctx.JSON(400, utils.ErrorStr("Invalid entity type"))
return
}
teamId := ctx.Param("teamid")
if teamId == "default" {
addDefaultMember(ctx, guildId, snowflake, entityType)
} else {
parsed, err := strconv.Atoi(teamId)
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid team ID"))
return
}
addTeamMember(ctx, parsed, guildId, snowflake, entityType)
}
}
func addDefaultMember(ctx *gin.Context, guildId, snowflake uint64, entityType entityType) {
var err error
switch entityType {
case entityTypeUser:
err = dbclient.Client.Permissions.AddSupport(guildId, snowflake)
case entityTypeRole:
err = dbclient.Client.RolePermissions.AddSupport(guildId, snowflake)
}
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}
func addTeamMember(ctx *gin.Context, teamId int, guildId, snowflake uint64, entityType entityType) {
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !exists {
ctx.JSON(404, utils.ErrorStr("Support team with provided ID not found"))
return
}
switch entityType {
case entityTypeUser:
err = dbclient.Client.SupportTeamMembers.Add(teamId, snowflake)
case entityTypeRole:
err = dbclient.Client.SupportTeamRoles.Add(teamId, snowflake)
}
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,39 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
)
func CreateTeam(ctx *gin.Context) {
type body struct {
Name string `json:"name"`
}
guildId := ctx.Keys["guildid"].(uint64)
var data body
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
if len(data.Name) == 0 || len(data.Name) > 32 {
ctx.JSON(400, utils.ErrorStr("Team name must be between 1 and 32 characters"))
return
}
id, err := dbclient.Client.SupportTeam.Create(guildId, data.Name)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, database.SupportTeam{
Id: id,
GuildId: guildId,
Name: data.Name,
})
}

View File

@ -0,0 +1,37 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func DeleteTeam(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
teamId, err := strconv.Atoi(ctx.Param("teamid"))
if err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
// check team belongs to guild
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !exists {
ctx.JSON(400, utils.ErrorStr("Team not found"))
return
}
if err := dbclient.Client.SupportTeam.Delete(teamId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,186 @@
package api
import (
"context"
"fmt"
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
syncutils "github.com/TicketsBot/common/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/user"
"golang.org/x/sync/errgroup"
"sort"
"strconv"
)
func GetMembers(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
teamId := ctx.Param("teamid")
if teamId == "default" {
getDefaultMembers(ctx, guildId)
} else {
parsed, err := strconv.Atoi(teamId)
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid team ID"))
return
}
getTeamMembers(ctx, parsed, guildId)
}
}
func getDefaultMembers(ctx *gin.Context, guildId uint64) {
group, _ := errgroup.WithContext(context.Background())
// get IDs of support users & roles
var userIds []uint64
group.Go(func() (err error) {
userIds, err = dbclient.Client.Permissions.GetSupport(guildId)
return
})
var roleIds []uint64
group.Go(func() (err error) {
roleIds, err = dbclient.Client.RolePermissions.GetSupportRoles(guildId)
return
})
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
data, err := formatMembers(guildId, userIds, roleIds)
if err == nil {
ctx.JSON(200, data)
} else {
ctx.JSON(500, utils.ErrorJson(err))
}
}
func getTeamMembers(ctx *gin.Context, teamId int, guildId uint64) {
// Verify team exists
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !exists {
ctx.JSON(404, utils.ErrorStr("Support team with provided ID not found"))
return
}
group, _ := errgroup.WithContext(context.Background())
// get IDs of support users & roles
var userIds []uint64
group.Go(func() (err error) {
userIds, err = dbclient.Client.SupportTeamMembers.Get(teamId)
return
})
var roleIds []uint64
group.Go(func() (err error) {
roleIds, err = dbclient.Client.SupportTeamRoles.Get(teamId)
return
})
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
data, err := formatMembers(guildId, userIds, roleIds)
if err == nil {
ctx.JSON(200, data)
} else {
ctx.JSON(500, utils.ErrorJson(err))
}
}
func formatMembers(guildId uint64, userIds, roleIds []uint64) ([]entity, error) {
ctx, err := botcontext.ContextForGuild(guildId)
if err != nil {
return nil, err
}
// get role objects so we can get name
roles, err := ctx.GetGuildRoles(guildId)
if err != nil {
return nil, err
}
// map role ids to names
var data []entity
for _, roleId := range roleIds {
for _, role := range roles {
if roleId == role.Id {
data = append(data, entity{
Id: roleId,
Name: role.Name,
Type: entityTypeRole,
})
break
}
}
}
// map user ids to names & discrims
group, _ := errgroup.WithContext(context.Background())
users := make(chan user.User)
wg := syncutils.NewChannelWaitGroup()
wg.Add(len(userIds))
for _, userId := range userIds {
userId := userId
group.Go(func() error {
defer wg.Done()
user, err := ctx.GetUser(userId)
if err != nil {
// TODO: Log w sentry
return nil // We should skip the error, since it's probably 403 / 404 etc
}
users <- user
return nil
})
}
group.Go(func() error {
loop:
for {
select {
case <-wg.Wait():
break loop
case user := <-users:
data = append(data, entity{
Id: user.Id,
Name: fmt.Sprintf("%s#%s", user.Username, user.PadDiscriminator()),
Type: entityTypeUser,
})
}
}
return nil
})
if err := group.Wait(); err != nil {
return nil, err
}
// sort
sort.Slice(data, func(i, j int) bool {
if data[i].Type == data[j].Type {
return data[i].Id < data[j].Id
} else {
return data[i].Type > data[j].Type
}
})
return data, nil
}

View File

@ -0,0 +1,25 @@
package api
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
)
func GetTeams(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
teams, err := dbclient.Client.SupportTeam.Get(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// prevent serving null
if teams == nil {
teams = make([]database.SupportTeam, 0)
}
ctx.JSON(200, teams)
}

View File

@ -0,0 +1,123 @@
package api
import (
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func RemoveMember(ctx *gin.Context) {
guildId, selfId := ctx.Keys["guildid"].(uint64), ctx.Keys["userid"].(uint64)
snowflake, err := strconv.ParseUint(ctx.Param("snowflake"), 10, 64)
if err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
// get entity type
typeParsed, err := strconv.Atoi(ctx.Query("type"))
if err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
entityType, ok := entityTypes[typeParsed]
if !ok {
ctx.JSON(400, utils.ErrorStr("Invalid entity type"))
return
}
teamId := ctx.Param("teamid")
if teamId == "default" {
removeDefaultMember(ctx, guildId, selfId, snowflake, entityType)
} else {
parsed, err := strconv.Atoi(teamId)
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid team ID"))
return
}
removeTeamMember(ctx, parsed, guildId, snowflake, entityType)
}
}
func removeDefaultMember(ctx *gin.Context, guildId, selfId, snowflake uint64, entityType entityType) {
// permission check
var isAdmin bool
var err error
switch entityType {
case entityTypeUser:
isAdmin, err = dbclient.Client.Permissions.IsAdmin(guildId, snowflake)
case entityTypeRole:
isAdmin, err = dbclient.Client.RolePermissions.IsAdmin(snowflake)
}
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// only guild owner can remove admins
if isAdmin {
botCtx, err := botcontext.ContextForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
guild, err := botCtx.GetGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if guild.OwnerId != selfId {
ctx.JSON(403, utils.ErrorStr("Only the server owner can remove admins"))
return
}
}
switch entityType {
case entityTypeUser:
err = dbclient.Client.Permissions.RemoveSupport(guildId, snowflake)
case entityTypeRole:
err = dbclient.Client.RolePermissions.RemoveSupport(guildId, snowflake)
}
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}
func removeTeamMember(ctx *gin.Context, teamId int, guildId, snowflake uint64, entityType entityType) {
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !exists {
ctx.JSON(404, utils.ErrorStr("Support team with provided ID not found"))
return
}
switch entityType {
case entityTypeUser:
err = dbclient.Client.SupportTeamMembers.Delete(teamId, snowflake)
case entityTypeRole:
err = dbclient.Client.SupportTeamRoles.Delete(teamId, snowflake)
}
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,19 @@
package api
type entityType int
const (
entityTypeUser entityType = iota
entityTypeRole
)
var entityTypes = map[int]entityType{
int(entityTypeUser): entityTypeUser,
int(entityTypeRole): entityTypeRole,
}
type entity struct {
Id uint64 `json:"id,string"`
Name string `json:"name"`
Type entityType `json:"type"`
}

View File

@ -61,7 +61,7 @@ func LogViewHandler(ctx *gin.Context) {
// Verify the user has permissions to be here // Verify the user has permissions to be here
permLevel, err := utils.GetPermissionLevel(guildId, userId) permLevel, err := utils.GetPermissionLevel(guildId, userId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -49,7 +49,7 @@ func AuthenticateGuild(isApiMethod bool, requiredPermissionLevel permission.Perm
permLevel, err := utils.GetPermissionLevel(guild.Id, userId) permLevel, err := utils.GetPermissionLevel(guild.Id, userId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return return
} }

View File

@ -9,6 +9,7 @@ import (
api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel" api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings" api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags" api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags"
api_team "github.com/TicketsBot/GoPanel/app/http/endpoints/api/team"
api_ticket "github.com/TicketsBot/GoPanel/app/http/endpoints/api/ticket" api_ticket "github.com/TicketsBot/GoPanel/app/http/endpoints/api/ticket"
api_whitelabel "github.com/TicketsBot/GoPanel/app/http/endpoints/api/whitelabel" 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/manage"
@ -89,6 +90,7 @@ func StartServer() {
guildAuthApiSupport.GET("/premium", api.PremiumHandler) guildAuthApiSupport.GET("/premium", api.PremiumHandler)
guildAuthApiSupport.GET("/user/:user", api.UserHandler) guildAuthApiSupport.GET("/user/:user", api.UserHandler)
guildAuthApiSupport.GET("/roles", api.RolesHandler) guildAuthApiSupport.GET("/roles", api.RolesHandler)
guildAuthApiSupport.GET("/members/search", createLimiter(10, time.Second * 30), createLimiter(75, time.Minute * 30), api.SearchMembers)
guildAuthApiAdmin.GET("/settings", api_settings.GetSettingsHandler) guildAuthApiAdmin.GET("/settings", api_settings.GetSettingsHandler)
guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler) guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler)
@ -123,6 +125,13 @@ func StartServer() {
guildAuthApiAdmin.GET("/autoclose", api_autoclose.GetAutoClose) guildAuthApiAdmin.GET("/autoclose", api_autoclose.GetAutoClose)
guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose) guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose)
guildAuthApiAdmin.GET("/team", api_team.GetTeams)
guildAuthApiAdmin.GET("/team/:teamid", createLimiter(5, time.Second * 15), api_team.GetMembers)
guildAuthApiAdmin.POST("/team", createLimiter(10, time.Minute), api_team.CreateTeam)
guildAuthApiAdmin.PUT("/team/:teamid/:snowflake", createLimiter(5, time.Second * 10), api_team.AddMember)
guildAuthApiAdmin.DELETE("/team/:teamid", api_team.DeleteTeam)
guildAuthApiAdmin.DELETE("/team/:teamid/:snowflake", createLimiter(30, time.Minute), api_team.RemoveMember)
} }
userGroup := router.Group("/user", middleware.AuthenticateToken) userGroup := router.Group("/user", middleware.AuthenticateToken)

View File

@ -10,6 +10,7 @@ import (
"github.com/rxdn/gdl/objects/channel" "github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild" "github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/member" "github.com/rxdn/gdl/objects/member"
"github.com/rxdn/gdl/objects/user"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/ratelimit" "github.com/rxdn/gdl/rest/ratelimit"
) )
@ -76,6 +77,15 @@ func (ctx BotContext) GetGuildMember(guildId, userId uint64) (m member.Member, e
return return
} }
func (ctx BotContext) GetUser(userId uint64) (u user.User, err error) {
u, err = rest.GetUser(ctx.Token, ctx.RateLimiter, userId)
if err == nil {
go cache.Instance.StoreUser(u)
}
return
}
func (ctx BotContext) GetGuildRoles(guildId uint64) (roles []guild.Role, err error) { func (ctx BotContext) GetGuildRoles(guildId uint64) (roles []guild.Role, err error) {
if roles := cache.Instance.GetGuildRoles(guildId); len(roles) > 0 { if roles := cache.Instance.GetGuildRoles(guildId); len(roles) > 0 {
return roles, nil return roles, nil
@ -88,3 +98,17 @@ func (ctx BotContext) GetGuildRoles(guildId uint64) (roles []guild.Role, err err
return return
} }
func (ctx BotContext) SearchMembers(guildId uint64, query string) (members []member.Member, err error) {
data := rest.SearchGuildMembersData{
Query: query,
Limit: 100,
}
members, err = rest.SearchGuildMembers(ctx.Token, ctx.RateLimiter, guildId, data)
if err == nil {
go cache.Instance.StoreMembers(members, guildId)
}
return
}

4
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-20200704164621-09d42dd941e0 github.com/TicketsBot/archiverclient v0.0.0-20200704164621-09d42dd941e0
github.com/TicketsBot/common v0.0.0-20210118172556-0b20b84f7df4 github.com/TicketsBot/common v0.0.0-20210118172556-0b20b84f7df4
github.com/TicketsBot/database v0.0.0-20210215164209-6ec5ebcbc399 github.com/TicketsBot/database v0.0.0-20210218163040-99158e109ab9
github.com/TicketsBot/worker v0.0.0-20210207182653-fabef254ea30 github.com/TicketsBot/worker v0.0.0-20210207182653-fabef254ea30
github.com/apex/log v1.1.2 github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
@ -21,7 +21,7 @@ require (
github.com/jackc/pgx/v4 v4.7.1 github.com/jackc/pgx/v4 v4.7.1
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rxdn/gdl v0.0.0-20210213145645-ea1107cdc3e1 github.com/rxdn/gdl v0.0.0-20210215161213-1eb4c25c602c
github.com/sirupsen/logrus v1.5.0 github.com/sirupsen/logrus v1.5.0
github.com/ulule/limiter/v3 v3.5.0 github.com/ulule/limiter/v3 v3.5.0
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a

View File

@ -75,7 +75,6 @@ button.close {
h1, .h1, h2, .h2, h3, .h3, h4, .h4 { h1, .h1, h2, .h2, h3, .h3, h4, .h4 {
font-weight: 300; font-weight: 300;
margin: 30px 0 15px;
} }
h1, .h1 { h1, .h1 {

View File

@ -2,7 +2,6 @@
body, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, .navbar, .brand, .btn-simple, .alert, a, .td-name, td, button.close { body, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, .navbar, .brand, .btn-simple, .alert, a, .td-name, td, button.close {
font-family: 'Noto Sans', sans-serif !important; font-family: 'Noto Sans', sans-serif !important;
font-weight: 400 !important; font-weight: 400 !important;
} }
@ -102,7 +101,6 @@ html > ::-webkit-scrollbar {
} }
.table td, .table th { .table td, .table th {
text-align: center;
} }
.close-container { .close-container {
@ -309,7 +307,6 @@ html > ::-webkit-scrollbar {
.flex-container { .flex-container {
display: flex; display: flex;
height: 100%;
width: 100%; width: 100%;
} }
@ -331,14 +328,17 @@ html > ::-webkit-scrollbar {
.tcontent-container { .tcontent-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 100%;
width: 100%; width: 100%;
} }
.tcontent-container {
display: none;
}
.team-card-container { .team-card-container {
display: flex; display: flex;
margin-top: 4%; margin: 4% 0;
height: 50%; min-height: 50%;
width: 80%; width: 80%;
} }
@ -346,7 +346,6 @@ html > ::-webkit-scrollbar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-evenly; justify-content: space-evenly;
height: 100%;
width: 100%; width: 100%;
background-color: #272727 !important; background-color: #272727 !important;
@ -371,7 +370,7 @@ html > ::-webkit-scrollbar {
.tcard-body { .tcard-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 2%; padding: 2%;
flex: 1; flex: 1;
width: 100%; width: 100%;
color: white; color: white;
@ -380,7 +379,120 @@ html > ::-webkit-scrollbar {
.flex-center { .flex-center {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.columns {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
}
.column {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
}
.team-item {
display: flex !important;
justify-content: space-between;
align-items: center;
color: white !important;
}
.add-search {
height: 100% !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.add-search-btn {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.inline {
display: flex;
width: 100%;
flex-direction: row;
}
.trow {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.search-dropdown, .search-dropdown:active, .search-dropdown:focus {
width: 100%;
background-color: #2e3136;
border-top: 1px solid rgba(0,0,0,.125);
border-left-color: #2e3136 !important;
border-right-color: #2e3136 !important;
border-bottom-color: #2e3136 !important;
border-radius: 0 !important;
scrollbar-color: dark;
outline: none;
overflow: auto;
}
.search-dropdown-item {
font-size: 16px;
color: white;
padding-left: 12px;
}
.search-dropdown-item:active, .search-dropdown-item::selection {
background-color: #272727;
}
.add-button {
width: 100%;
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.25);
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
#user-dropdown-wrapper, #role-dropdown-wrapper {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 100;
}
.float-right {
float: right;
}
.team-creation-form {
display: flex;
width: 100%;
flex-direction: row;
}
.team-creation-name {
display: flex;
width: 80% !important;
max-width: 320px;
height: 100% !important;
}
.delete-team-icon {
float: right;
cursor: pointer;
vertical-align: center;
}
#notificationmodal {
z-index: 10000 !important;
}

View File

@ -1,5 +1,5 @@
function showLoadingScreen() { function showLoadingScreen() {
const content = document.getElementsByClassName('content')[0]; const content = document.getElementsByClassName('content')[0] || document.getElementsByClassName('tcontent-container')[0];
content.style.display = 'none'; content.style.display = 'none';
document.getElementById('loading-container').style.display = 'block'; document.getElementById('loading-container').style.display = 'block';
} }
@ -7,8 +7,13 @@ function showLoadingScreen() {
function hideLoadingScreen() { function hideLoadingScreen() {
document.getElementById('loading-container').style.display = 'none'; document.getElementById('loading-container').style.display = 'none';
const content = document.getElementsByClassName('content')[0]; const content = document.getElementsByClassName('content')[0] || document.getElementsByClassName('tcontent-container')[0];
if (content.classList.contains('tcontent-container')) {
content.style.display = 'flex';
} else {
content.style.display = 'block'; content.style.display = 'block';
}
content.classList.add('fade-in'); content.classList.add('fade-in');
} }

View File

@ -26,12 +26,12 @@ function appendTd(tr, content) {
return td return td
} }
function appendButton(tr, content, onclick) { function appendButton(tr, content, onclick, ...classList) {
const tdRemove = document.createElement('td'); const tdRemove = document.createElement('td');
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'submit'; btn.type = 'submit';
btn.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto'); btn.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto', ...classList);
btn.appendChild(document.createTextNode(content)); btn.appendChild(document.createTextNode(content));
btn.onclick = onclick; btn.onclick = onclick;
@ -59,3 +59,9 @@ function prependChild(parent, child) {
parent.insertBefore(child, parent.children[0]); parent.insertBefore(child, parent.children[0]);
} }
} }
function createElement(tag, ...classList) {
const el = document.createElement(tag);
el.classList.add(...classList);
return el;
}

View File

@ -45,6 +45,10 @@
notify('Success', message); notify('Success', message);
} }
function notifyRatelimit() {
notifyError("You're doing that too fast: please wait a few seconds and try again");
}
function closeNotificationModal() { function closeNotificationModal() {
$('#notificationmodal').modal('hide'); $('#notificationmodal').modal('hide');
} }

View File

@ -82,13 +82,20 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12 pr-1"> <div class="col-md-6 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black">Mention On Open</label> <label class="black">Mention On Open</label>
<select class="selectpicker form-control" id="edit-mentions" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static"> <select class="selectpicker form-control" id="edit-mentions" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static">
</select> </select>
</div> </div>
</div> </div>
<div class="col-md-6 pr-1">
<div class="form-group">
<label class="black">Support Teams</label>
<select class="selectpicker form-control" id="edit-teams" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static">
</select>
</div>
</div>
</div> </div>
<input type="hidden" id="edit-message-id"> <input type="hidden" id="edit-message-id">
@ -150,10 +157,12 @@
fillChannels('edit-channel-container', channels); fillChannels('edit-channel-container', channels);
fillCategories('edit-category-container', channels); fillCategories('edit-category-container', channels);
await fillMentions('edit-mentions'); await fillMentions('edit-mentions');
await fillTeams('edit-teams');
setActiveChannel('edit-channel-container', panel.channel_id); setActiveChannel('edit-channel-container', panel.channel_id);
setActiveCategory(panel); setActiveCategory(panel);
setActiveMentions(panel); setActiveMentions(panel);
setActiveTeams(panel);
} }
function setActiveCategory(panel) { function setActiveCategory(panel) {
@ -172,6 +181,21 @@
} }
} }
function setActiveTeams(panel) {
const values = [];
if (panel['default_team']) {
values.push('default');
$('#edit-teams').selectpicker('val', 'default');
}
if (panel.teams) {
values.push(...panel.teams.map((t) => t.id));
}
$('#edit-teams').selectpicker('val', values);
}
async function updatePanel() { async function updatePanel() {
const messageId = document.getElementById('edit-message-id').value; const messageId = document.getElementById('edit-message-id').value;
const title = document.getElementById('edit-title').value; const title = document.getElementById('edit-title').value;
@ -188,7 +212,8 @@
channel_id: document.getElementById('edit-channel-container').options[document.getElementById('edit-channel-container').selectedIndex].value, channel_id: document.getElementById('edit-channel-container').options[document.getElementById('edit-channel-container').selectedIndex].value,
category_id: document.getElementById('edit-category-container').options[document.getElementById('edit-category-container').selectedIndex].value, category_id: document.getElementById('edit-category-container').options[document.getElementById('edit-category-container').selectedIndex].value,
welcome_message: welcomeMessage === '' ? null : welcomeMessage, welcome_message: welcomeMessage === '' ? null : welcomeMessage,
mentions: $('#edit-mentions').val() mentions: $('#edit-mentions').val(),
teams: $('#edit-teams').val()
}; };
const res = await axios.put('/api/{{.guildId}}/panels/' + messageId, data); const res = await axios.put('/api/{{.guildId}}/panels/' + messageId, data);

View File

@ -124,7 +124,7 @@
<div class="collapse" id="advanced" style="width: 100%"> <div class="collapse" id="advanced" style="width: 100%">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-6 pr-1"> <div class="col-md-12 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black">Welcome Message <label class="black">Welcome Message
<i class="fas fa-question pointer" onclick="showSubstitutionModal()"></i> <i class="fas fa-question pointer" onclick="showSubstitutionModal()"></i>
@ -132,7 +132,8 @@
<textarea type="text" class="form-control" placeholder="If not provided, your server's default welcome message will be used" id="welcome_message"></textarea> <textarea type="text" class="form-control" placeholder="If not provided, your server's default welcome message will be used" id="welcome_message"></textarea>
</div> </div>
</div> </div>
</div>
<div class="row">
<div class="col-md-6 pr-1"> <div class="col-md-6 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black" for="mentions">Mention On Open</label> <label class="black" for="mentions">Mention On Open</label>
@ -140,6 +141,13 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-md-6 pr-1">
<div class="form-group">
<label class="black" for="teams">Support Teams</label>
<select class="selectpicker form-control" id="teams" multiple data-live-search="true" data-dropup-auto="false" data-size="5" data-display="static">
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -287,7 +295,8 @@
channel_id: document.getElementById('channel-container').options[document.getElementById('channel-container').selectedIndex].value, channel_id: document.getElementById('channel-container').options[document.getElementById('channel-container').selectedIndex].value,
category_id: document.getElementById('category-container').options[document.getElementById('category-container').selectedIndex].value, category_id: document.getElementById('category-container').options[document.getElementById('category-container').selectedIndex].value,
welcome_message: welcomeMessage === '' ? null : welcomeMessage, welcome_message: welcomeMessage === '' ? null : welcomeMessage,
mentions: $('#mentions').val() mentions: $('#mentions').val(),
teams: $('#teams').val()
}; };
const res = await axios.put('/api/{{.guildId}}/panels', data); const res = await axios.put('/api/{{.guildId}}/panels', data);
@ -476,6 +485,7 @@
async function fillMentions(elementId) { async function fillMentions(elementId) {
const select = document.getElementById(elementId); const select = document.getElementById(elementId);
select.innerHTML = '';
// ticket opener // ticket opener
const ticketOpener = document.createElement('option'); const ticketOpener = document.createElement('option');
@ -502,6 +512,33 @@
$(`#${elementId}`).selectpicker('refresh'); $(`#${elementId}`).selectpicker('refresh');
} }
async function fillTeams(elementId) {
const select = document.getElementById(elementId);
select.innerHTML = '';
const defaultTeam = createElement('option');
defaultTeam.value = 'default';
defaultTeam.appendChild(document.createTextNode('Default'));
select.appendChild(defaultTeam);
const res = await axios.get('/api/{{.guildId}}/team');
if (res.status !== 200) {
notifyError(res.data);
return;
}
if (res.data) {
for (const team of res.data) {
const option = document.createElement('option');
option.value = team.id;
option.appendChild(document.createTextNode(team.name));
select.appendChild(option);
}
}
$(`#${elementId}`).selectpicker('refresh');
}
async function loadData() { async function loadData() {
const channels = await getChannels(); const channels = await getChannels();
const panelCount = await fillPanels(channels); const panelCount = await fillPanels(channels);
@ -513,6 +550,9 @@
fillChannels('multi-channel-container', channels); fillChannels('multi-channel-container', channels);
fillCategories('category-container', channels); fillCategories('category-container', channels);
await fillMentions('mentions'); await fillMentions('mentions');
await fillTeams('teams');
$(`#teams`).selectpicker('val', 'default');
} }
withLoadingScreen(loadData); withLoadingScreen(loadData);

View File

@ -6,14 +6,343 @@
<span>Support Teams</span> <span>Support Teams</span>
</div> </div>
<div class="tcard-body"> <div class="tcard-body">
<h2><b>Create Team</b></h2>
<div id="team-creation-wrapper">
<form class="team-creation-form" onsubmit="createTeam(); return false">
<input type="text" class="form-control team-creation-name" id="team-creation-name" placeholder="Team Name">
<button class="btn btn-primary btn-fill" type="submit" style="margin-left: 2%"><i class="fas fa-paper-plane"></i> Create</button>
</form>
</div>
<h2 style="margin-top: 2%"><b>Manage Teams</b></h2>
<div class="flex-center"> <div class="flex-center">
<label for="team-selection">Select a team</label> <div class="dropdown" style="width: 100%">
<select name="team-selection" style="width: 100%"> <button class="btn btn-primary btn-fill dropdown-toggle" type="button" id="teamDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="width: 100%">
<option value="default">Default</option> Select a team
</select> </button>
<div class="dropdown-menu" id="team-selector" aria-labelledby="teamDropdown" style="width: 100%">
</div>
</div>
<div class="columns">
<div class="column" style="padding-right: 1%">
<h3>Manage Members</h3>
<table class="table table-hover">
<tbody id="members-body">
</tbody>
</table>
</div>
<div class="column" style="padding-left: 1%">
<div class="trow" style="position: relative">
<h3>Add Member</h3>
<div class="inline">
<input type="text" class="form-control add-search" id="add-search-user" placeholder="Username">
<button type="button" class="btn btn-primary btn-fill add-search-btn" id="user-search-btn" onclick="searchUser()"><i class="fas fa-search search-icon"></i></button>
</div>
<div id="user-dropdown-wrapper">
<select id="add-search-user-dropdown" class="search-dropdown" size="4"></select>
<button type="button" class="btn btn-primary btn-fill add-button" onclick="addUser()"><i class="fas fa-plus"></i> Add Selected User</button>
</div>
</div>
<div class="trow" style="position:relative;">
<h3>Add Role</h3>
<div class="inline">
<input
type="text" class="form-control add-search" id="add-search-role" placeholder="Role Name"
oninput="updateRoleSearch()" onfocusin="showRoleDropdown()">
<div id="role-dropdown-wrapper">
<select id="add-search-role-dropdown" class="search-dropdown" size="4"></select>
<button type="button" class="btn btn-primary btn-fill add-button" onclick="addRole()"><i class="fas fa-plus"></i> Add Selected Role</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
</div>
<script>
let activeTeam = 'default';
let roles = [];
function showUserDropdown() {
const dropdown = document.getElementById('user-dropdown-wrapper');
dropdown.style.display = 'block';
const input = document.getElementById('add-search-user');
input.style.borderBottomLeftRadius = '0';
const searchBtn = document.getElementById('user-search-btn');
searchBtn.style.borderBottomRightRadius = '0';
}
function hideUserDropdown() {
const dropdown = document.getElementById('user-dropdown-wrapper');
dropdown.style.display = 'none';
const input = document.getElementById('add-search-user');
input.style.borderBottomLeftRadius = '4px';
const searchBtn = document.getElementById('user-search-btn');
searchBtn.style.borderBottomRightRadius = '0.25rem';
}
function showRoleDropdown() {
const dropdown = document.getElementById('role-dropdown-wrapper');
dropdown.style.display = 'block';
const input = document.getElementById('add-search-role');
input.style.borderBottomLeftRadius = '0';
updateRoleSearch();
}
function hideRoleDropdown() {
const dropdown = document.getElementById('role-dropdown-wrapper');
dropdown.style.display = 'none';
const input = document.getElementById('add-search-role');
input.style.borderBottomLeftRadius = '4px';
}
function updateRoleSearch() {
const input = document.getElementById('add-search-role');
const dropdown = document.getElementById('add-search-role-dropdown');
dropdown.innerHTML = '';
roles.filter((role) => role.name.toLowerCase().includes(input.value.toLowerCase())).forEach((role) => {
const option = createElement('option', 'search-dropdown-item');
option.id = role.id;
option.appendChild(document.createTextNode(role.name));
dropdown.appendChild(option);
});
}
async function searchUser() {
showUserDropdown();
const query = document.getElementById('add-search-user').value;
const res = await axios.get('/api/{{.guildId}}/members/search?query=' + encodeURIComponent(query));
if (res.status !== 200) {
if (res.status === 429){
notifyRatelimit();
} else {
notifyError(res.data.error);
}
return;
}
const dropdown = document.getElementById('add-search-user-dropdown');
dropdown.innerHTML = '';
for (const member of res.data) {
const option = createElement('option', 'search-dropdown-item');
option.id = member.user.id;
option.appendChild(document.createTextNode(`${member.user.username}#${member.user.discriminator}`));
dropdown.appendChild(option);
}
}
async function addUser() {
const dropdown = document.getElementById('add-search-user-dropdown');
const option = dropdown.options[dropdown.selectedIndex];
if (option === undefined) {
notifyError("You need to select a user");
return;
}
const res = await axios.put('/api/{{.guildId}}/team/' + activeTeam + '/' + option.id + '?type=0');
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
await displayTeam(activeTeam);
notifySuccess("Added user to support team!")
}
async function addRole() {
const dropdown = document.getElementById('add-search-role-dropdown');
const option = dropdown.options[dropdown.selectedIndex];
if (option === undefined) {
notifyError("You need to select a role");
return;
}
const res = await axios.put('/api/{{.guildId}}/team/' + activeTeam + '/' + option.id + '?type=1');
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
await displayTeam(activeTeam);
notifySuccess("Added role to support team!")
}
// hide user dropdown
window.addEventListener('click', (e) => {
const allowedElements = ['search-dropdown', 'search-dropdown-item', 'add-button', 'add-search-btn', 'add-search', 'search-icon'];
const wrapper = document.getElementById('user-dropdown-wrapper');
if (window.getComputedStyle(wrapper).display !== 'none') {
if(!allowedElements.some((clazz) => e.target.classList.contains(clazz))) {
hideUserDropdown();
}
}
});
// hide role dropdown
window.addEventListener('click', (e) => {
const allowedElements = ['search-dropdown', 'search-dropdown-item', 'add-button', 'add-search'];
const wrapper = document.getElementById('role-dropdown-wrapper');
if (window.getComputedStyle(wrapper).display !== 'none') {
if(!allowedElements.some((clazz) => e.target.classList.contains(clazz))) {
hideRoleDropdown();
}
}
});
async function displayTeam(id) {
const res = await axios.get('/api/{{.guildId}}/team/' + id);
if (res.status !== 200) {
if (res.status === 429){
notifyRatelimit();
} else {
notifyError(res.data.error);
}
return;
}
activeTeam = id;
const table = document.getElementById('members-body');
table.innerHTML = '';
if (res.data) {
for (const entity of res.data) {
const tr = document.createElement('tr');
appendTd(tr, entity.name);
appendButton(tr, "Remove", () => removeMember(id, entity.id, entity.type), 'float-right');
table.appendChild(tr);
}
}
}
async function removeMember(teamId, snowflake, entityType) {
const res = await axios.delete('/api/{{.guildId}}/team/' + teamId + '/' + snowflake + '?type=' + entityType);
if (res.status !== 200) {
if (res.status === 429){
notifyRatelimit();
} else {
notifyError(res.data.error);
}
return;
}
if (entityType === 0) { // user
notifySuccess("Support user removed successfully")
} else if (entityType === 1) { //role
notifySuccess("Support role removed successfully")
}
await displayTeam(activeTeam);
}
async function createTeam() {
const input = document.getElementById('team-creation-name');
const data = {
name: input.value
};
const res = await axios.post('/api/{{.guildId}}/team', data);
if (res.status !== 200) {
if (res.status === 429){
notifyRatelimit();
} else {
notifyError(res.data.error);
}
return;
}
input.value = '';
await loadTeams();
notifySuccess("Team created successfully");
}
async function deleteTeam(id) {
const res = await axios.delete('/api/{{.guildId}}/team/' + id);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
notifySuccess("Team deleted successfully");
await displayTeam('default');
}
function appendTeam(id, name) {
const wrapper = document.getElementById('team-selector');
const option = createElement('a', 'dropdown-item', 'team-item');
option.onclick = () => displayTeam(id);
option.appendChild(document.createTextNode(name));
if (id !== 'default') {
const deleteIcon = createElement('i', 'fas', 'fa-window-close', 'delete-team-icon');
deleteIcon.onclick = () => deleteTeam(id);
option.appendChild(deleteIcon);
}
wrapper.appendChild(option);
}
async function loadTeams() {
const res = await axios.get('/api/{{.guildId}}/team');
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
const wrapper = document.getElementById('team-selector');
wrapper.innerHTML = '';
appendTeam('default', 'Default');
for (const team of res.data) {
appendTeam(team.id, team.name);
}
}
async function loadRoles() {
const res = await axios.get('/api/{{.guildId}}/roles');
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
roles = res.data.roles;
}
async function loadData() {
await loadRoles();
await loadTeams();
await displayTeam('default');
}
withLoadingScreen(loadData);
</script>
{{end}} {{end}}

View File

@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func ErrorToResponse(err error) map[string]interface{} { func ErrorJson(err error) map[string]interface{} {
return ErrorStr(err.Error()) return ErrorStr(err.Error())
} }

View File

@ -2,22 +2,14 @@ package utils
import ( import (
"github.com/rxdn/gdl/objects/channel/message" "github.com/rxdn/gdl/objects/channel/message"
"reflect"
) )
func Contains(s interface{}, elem interface{}) bool { func ContainsString(slice []string, target string) bool {
arrV := reflect.ValueOf(s) for _, elem := range slice {
if elem == target {
if arrV.Kind() == reflect.Slice {
for i := 0; i < arrV.Len(); i++ {
// XXX - panics if slice element points to an unexported struct field
// see https://golang.org/pkg/reflect/#Value.Interface
if arrV.Index(i).Interface() == elem {
return true return true
} }
} }
}
return false return false
} }