From 741e0411901b956f0daae09c46730149ea522022 Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Thu, 18 Feb 2021 16:31:23 +0000 Subject: [PATCH] Support teams --- app/http/endpoints/api/getpermissionlevel.go | 2 +- .../endpoints/api/panel/multipanelcreate.go | 16 +- .../endpoints/api/panel/multipaneldelete.go | 14 +- .../endpoints/api/panel/multipanellist.go | 4 +- .../endpoints/api/panel/multipanelupdate.go | 30 +- app/http/endpoints/api/panel/panelcreate.go | 91 ++++- app/http/endpoints/api/panel/panellist.go | 66 ++-- app/http/endpoints/api/panel/panelupdate.go | 47 ++- app/http/endpoints/api/reloadguilds.go | 6 +- app/http/endpoints/api/searchmembers.go | 31 ++ app/http/endpoints/api/team/addmember.go | 88 +++++ app/http/endpoints/api/team/create.go | 39 ++ app/http/endpoints/api/team/delete.go | 37 ++ app/http/endpoints/api/team/getmembers.go | 186 ++++++++++ app/http/endpoints/api/team/getteams.go | 25 ++ app/http/endpoints/api/team/removemember.go | 123 +++++++ app/http/endpoints/api/team/types.go | 19 + app/http/endpoints/manage/logsview.go | 2 +- app/http/middleware/authenticateguild.go | 2 +- app/http/server.go | 9 + botcontext/botcontext.go | 24 ++ go.mod | 4 +- .../static/css/light-bootstrap-dashboard.css | 1 - public/static/css/style.css | 130 ++++++- public/static/js/loadingscreen.js | 11 +- public/static/js/utils.js | 10 +- public/templates/includes/notifymodal.tmpl | 4 + public/templates/includes/paneleditmodal.tmpl | 29 +- public/templates/views/panels.tmpl | 46 ++- public/templates/views/teams.tmpl | 337 +++++++++++++++++- utils/requestutils.go | 2 +- utils/sliceutils.go | 16 +- 32 files changed, 1312 insertions(+), 139 deletions(-) create mode 100644 app/http/endpoints/api/searchmembers.go create mode 100644 app/http/endpoints/api/team/addmember.go create mode 100644 app/http/endpoints/api/team/create.go create mode 100644 app/http/endpoints/api/team/delete.go create mode 100644 app/http/endpoints/api/team/getmembers.go create mode 100644 app/http/endpoints/api/team/getteams.go create mode 100644 app/http/endpoints/api/team/removemember.go create mode 100644 app/http/endpoints/api/team/types.go diff --git a/app/http/endpoints/api/getpermissionlevel.go b/app/http/endpoints/api/getpermissionlevel.go index 4fd5bf9..0ad1a18 100644 --- a/app/http/endpoints/api/getpermissionlevel.go +++ b/app/http/endpoints/api/getpermissionlevel.go @@ -46,7 +46,7 @@ func GetPermissionLevel(ctx *gin.Context) { } if err := group.Wait(); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/panel/multipanelcreate.go b/app/http/endpoints/api/panel/multipanelcreate.go index a582f7f..af51dd7 100644 --- a/app/http/endpoints/api/panel/multipanelcreate.go +++ b/app/http/endpoints/api/panel/multipanelcreate.go @@ -33,14 +33,14 @@ func MultiPanelCreate(ctx *gin.Context) { var data multiPanelCreateData if err := ctx.ShouldBindJSON(&data); err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } // validate body & get sub-panels panels, err := data.doValidations(guildId) if err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } @@ -61,9 +61,9 @@ func MultiPanelCreate(ctx *gin.Context) { if err != nil { var unwrapped request.RestError 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 { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) } return @@ -72,9 +72,9 @@ func MultiPanelCreate(ctx *gin.Context) { if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil { var unwrapped request.RestError 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 { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) } return @@ -91,7 +91,7 @@ func MultiPanelCreate(ctx *gin.Context) { multiPanel.Id, err = dbclient.Client.MultiPanels.Create(multiPanel) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } @@ -105,7 +105,7 @@ func MultiPanelCreate(ctx *gin.Context) { } if err := group.Wait(); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/panel/multipaneldelete.go b/app/http/endpoints/api/panel/multipaneldelete.go index 7111b77..38cf4be 100644 --- a/app/http/endpoints/api/panel/multipaneldelete.go +++ b/app/http/endpoints/api/panel/multipaneldelete.go @@ -16,42 +16,42 @@ func MultiPanelDelete(ctx *gin.Context) { multiPanelId, err := strconv.Atoi(ctx.Param("panelid")) if err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } // get bot context botContext, err := botcontext.ContextForGuild(guildId) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(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"))) + ctx.JSON(404, utils.ErrorJson(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"))) + ctx.JSON(403, utils.ErrorJson(errors.New("Guild ID doesn't match"))) return } var unwrapped request.RestError 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 } success, err := dbclient.Client.MultiPanels.Delete(guildId, multiPanelId) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } 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 } diff --git a/app/http/endpoints/api/panel/multipanellist.go b/app/http/endpoints/api/panel/multipanellist.go index dc83766..1a9ccce 100644 --- a/app/http/endpoints/api/panel/multipanellist.go +++ b/app/http/endpoints/api/panel/multipanellist.go @@ -19,7 +19,7 @@ func MultiPanelList(ctx *gin.Context) { multiPanels, err := dbclient.Client.MultiPanels.GetByGuild(guildId) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } @@ -45,7 +45,7 @@ func MultiPanelList(ctx *gin.Context) { } if err := group.Wait(); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/panel/multipanelupdate.go b/app/http/endpoints/api/panel/multipanelupdate.go index 29e295d..0ef977e 100644 --- a/app/http/endpoints/api/panel/multipanelupdate.go +++ b/app/http/endpoints/api/panel/multipanelupdate.go @@ -22,54 +22,54 @@ func MultiPanelUpdate(ctx *gin.Context) { // parse body var data multiPanelCreateData if err := ctx.ShouldBindJSON(&data); err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } // parse panel ID panelId, err := strconv.Atoi(ctx.Param("panelid")) if err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } // retrieve panel from DB multiPanel, ok, err := dbclient.Client.MultiPanels.Get(panelId) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } // check panel exists 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 } // check panel is in the same guild 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 } // validate body & get sub-panels panels, err := data.doValidations(guildId) if err != nil { - ctx.JSON(400, utils.ErrorToResponse(err)) + ctx.JSON(400, utils.ErrorJson(err)) return } // get bot context botContext, err := botcontext.ContextForGuild(guildId) if err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } // delete old message var unwrapped request.RestError 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 } @@ -81,9 +81,9 @@ func MultiPanelUpdate(ctx *gin.Context) { if err != nil { var unwrapped request.RestError 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 { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) } return @@ -93,9 +93,9 @@ func MultiPanelUpdate(ctx *gin.Context) { if err := data.addReactions(&botContext, data.ChannelId, messageId, panels); err != nil { var unwrapped request.RestError 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 { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) } return @@ -113,14 +113,14 @@ func MultiPanelUpdate(ctx *gin.Context) { } if err = dbclient.Client.MultiPanels.Update(multiPanel.Id, updated); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(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)) + ctx.JSON(500, utils.ErrorJson(err)) return } @@ -135,7 +135,7 @@ func MultiPanelUpdate(ctx *gin.Context) { } if err := group.Wait(); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/panel/panelcreate.go b/app/http/endpoints/api/panel/panelcreate.go index a94bb34..89ac0a3 100644 --- a/app/http/endpoints/api/panel/panelcreate.go +++ b/app/http/endpoints/api/panel/panelcreate.go @@ -1,7 +1,9 @@ package api import ( + "context" "errors" + "fmt" "github.com/TicketsBot/GoPanel/botcontext" dbclient "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/rpc" @@ -15,13 +17,28 @@ import ( "github.com/rxdn/gdl/objects/channel/message" "github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest/request" + "golang.org/x/sync/errgroup" "strconv" "strings" ) 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) { + guildId := ctx.Keys["guildid"].(uint64) botContext, err := botcontext.ContextForGuild(guildId) @@ -33,7 +50,7 @@ func CreatePanel(ctx *gin.Context) { return } - var data panel + var data panelBody if err := ctx.BindJSON(&data); err != nil { ctx.AbortWithStatusJSON(400, gin.H{ @@ -111,15 +128,16 @@ func CreatePanel(ctx *gin.Context) { // Store in DB panel := database.Panel{ - MessageId: msgId, - ChannelId: data.ChannelId, - GuildId: guildId, - Title: data.Title, - Content: data.Content, - Colour: int32(data.Colour), - TargetCategory: data.CategoryId, - ReactionEmote: emoji, - WelcomeMessage: data.WelcomeMessage, + MessageId: msgId, + ChannelId: data.ChannelId, + GuildId: guildId, + Title: data.Title, + Content: data.Content, + Colour: int32(data.Colour), + TargetCategory: data.CategoryId, + ReactionEmote: emoji, + WelcomeMessage: data.WelcomeMessage, + WithDefaultTeam: utils.ContainsString(data.Teams, "default"), } 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{ "success": true, "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() { ctx.AbortWithStatusJSON(400, gin.H{ "success": false, @@ -225,22 +280,22 @@ func (p *panel) doValidations(ctx *gin.Context, guildId uint64) bool { return true } -func (p *panel) verifyTitle() bool { +func (p *panelBody) verifyTitle() bool { 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 } -func (p *panel) getEmoji() (emoji string, ok bool) { +func (p *panelBody) getEmoji() (emoji string, ok bool) { p.Emote = strings.Replace(p.Emote, ":", "", -1) emoji, ok = utils.GetEmoji(p.Emote) return } -func (p *panel) verifyChannel(channels []channel.Channel) bool { +func (p *panelBody) verifyChannel(channels []channel.Channel) bool { var valid bool for _, ch := range channels { if ch.Id == p.ChannelId && ch.Type == channel.ChannelTypeGuildText { @@ -252,7 +307,7 @@ func (p *panel) verifyChannel(channels []channel.Channel) bool { return valid } -func (p *panel) verifyCategory(channels []channel.Channel) bool { +func (p *panelBody) verifyCategory(channels []channel.Channel) bool { var valid bool for _, ch := range channels { if ch.Id == p.CategoryId && ch.Type == channel.ChannelTypeGuildCategory { @@ -264,11 +319,11 @@ func (p *panel) verifyCategory(channels []channel.Channel) bool { return valid } -func (p *panel) verifyWelcomeMessage() bool { +func (p *panelBody) verifyWelcomeMessage() bool { 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(). SetTitle(p.Title). SetDescription(p.Content). diff --git a/app/http/endpoints/api/panel/panellist.go b/app/http/endpoints/api/panel/panellist.go index 931bbaa..e94a7f6 100644 --- a/app/http/endpoints/api/panel/panellist.go +++ b/app/http/endpoints/api/panel/panellist.go @@ -2,28 +2,31 @@ package api import ( "context" - "github.com/TicketsBot/GoPanel/database" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/database" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" "strconv" ) -type panel 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"` -} - func ListPanels(ctx *gin.Context) { + type panelResponse 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"` + WithDefaultTeam bool `json:"default_team"` + Teams []database.SupportTeam `json:"teams"` + } + guildId := ctx.Keys["guildid"].(uint64) - panels, err := database.Client.Panel.GetByGuild(guildId) + panels, err := dbclient.Client.Panel.GetByGuild(guildId) if err != nil { ctx.AbortWithStatusJSON(500, gin.H{ "success": false, @@ -32,7 +35,7 @@ func ListPanels(ctx *gin.Context) { return } - wrapped := make([]panel, len(panels)) + wrapped := make([]panelResponse, len(panels)) // we will need to lookup role mentions group, _ := errgroup.WithContext(context.Background()) @@ -45,7 +48,7 @@ func ListPanels(ctx *gin.Context) { var mentions []string // get role mentions - roles, err := database.Client.PanelRoleMentions.GetRoles(p.MessageId) + roles, err := dbclient.Client.PanelRoleMentions.GetRoles(p.MessageId) if err != nil { return err } @@ -56,7 +59,7 @@ func ListPanels(ctx *gin.Context) { } // 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 { return err } @@ -65,16 +68,23 @@ func ListPanels(ctx *gin.Context) { mentions = append(mentions, "user") } - wrapped[i] = panel{ - MessageId: p.MessageId, - ChannelId: p.ChannelId, - Title: p.Title, - Content: p.Content, - Colour: uint32(p.Colour), - CategoryId: p.TargetCategory, - Emote: p.ReactionEmote, - WelcomeMessage: p.WelcomeMessage, - Mentions: mentions, + teams, err := dbclient.Client.PanelTeams.GetTeams(p.MessageId) + if err != nil { + return err + } + + wrapped[i] = panelResponse{ + MessageId: p.MessageId, + ChannelId: p.ChannelId, + Title: p.Title, + Content: p.Content, + Colour: uint32(p.Colour), + CategoryId: p.TargetCategory, + Emote: p.ReactionEmote, + WelcomeMessage: p.WelcomeMessage, + Mentions: mentions, + WithDefaultTeam: p.WithDefaultTeam, + Teams: teams, } return nil @@ -84,7 +94,7 @@ func ListPanels(ctx *gin.Context) { if err := group.Wait(); err != nil { ctx.JSON(500, gin.H{ "success": false, - "error": err.Error(), + "error": err.Error(), }) return } diff --git a/app/http/endpoints/api/panel/panelupdate.go b/app/http/endpoints/api/panel/panelupdate.go index 593a4e9..37d7a56 100644 --- a/app/http/endpoints/api/panel/panelupdate.go +++ b/app/http/endpoints/api/panel/panelupdate.go @@ -22,20 +22,20 @@ func UpdatePanel(ctx *gin.Context) { botContext, err := botcontext.ContextForGuild(guildId) if err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } - var data panel + var data panelBody if err := ctx.BindJSON(&data); err != nil { - ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(400, utils.ErrorJson(err)) return } messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64) if err != nil { - ctx.AbortWithStatusJSON(400, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(400, utils.ErrorJson(err)) return } @@ -44,7 +44,7 @@ func UpdatePanel(ctx *gin.Context) { // get existing existing, err := dbclient.Client.Panel.Get(data.MessageId) if err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } @@ -65,7 +65,7 @@ func UpdatePanel(ctx *gin.Context) { // 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)) + ctx.JSON(500, utils.ErrorJson(err)) return } @@ -103,13 +103,13 @@ func UpdatePanel(ctx *gin.Context) { } if err := group.Wait(); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(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"))) + ctx.JSON(400, utils.ErrorJson(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel"))) return } @@ -144,7 +144,7 @@ func UpdatePanel(ctx *gin.Context) { }) } else { // TODO: Most appropriate error? - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) } return @@ -160,7 +160,7 @@ func UpdatePanel(ctx *gin.Context) { }) } else { // TODO: Most appropriate error? - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) } return @@ -178,23 +178,24 @@ func UpdatePanel(ctx *gin.Context) { TargetCategory: data.CategoryId, ReactionEmote: emoji, WelcomeMessage: data.WelcomeMessage, + WithDefaultTeam: utils.ContainsString(data.Teams, "default"), } if err = dbclient.Client.Panel.Update(messageId, panel); err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } // insert role mention data // delete old data if err = dbclient.Client.PanelRoleMentions.DeleteAll(newMessageId); err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } // TODO: Reduce to 1 query if err = dbclient.Client.PanelUserMention.Set(newMessageId, false); err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } @@ -202,13 +203,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, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } } else { roleId, err := strconv.ParseUint(mention, 10, 64) if err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } @@ -216,12 +217,26 @@ 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, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) 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{ "success": true, "message_id": strconv.FormatUint(newMessageId, 10), diff --git a/app/http/endpoints/api/reloadguilds.go b/app/http/endpoints/api/reloadguilds.go index 21de40d..60989be 100644 --- a/app/http/endpoints/api/reloadguilds.go +++ b/app/http/endpoints/api/reloadguilds.go @@ -16,14 +16,14 @@ func ReloadGuildsHandler(ctx *gin.Context) { key := fmt.Sprintf("tickets:dashboard:guildreload:%d", userId) res, err := messagequeue.Client.SetNX(key, 1, time.Second*10).Result() if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } if !res { ttl, err := messagequeue.Client.TTL(key).Result() if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } @@ -66,7 +66,7 @@ func ReloadGuildsHandler(ctx *gin.Context) { } if err := utils.LoadGuilds(accessToken, userId); err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/searchmembers.go b/app/http/endpoints/api/searchmembers.go new file mode 100644 index 0000000..7d2d191 --- /dev/null +++ b/app/http/endpoints/api/searchmembers.go @@ -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) +} diff --git a/app/http/endpoints/api/team/addmember.go b/app/http/endpoints/api/team/addmember.go new file mode 100644 index 0000000..ee61783 --- /dev/null +++ b/app/http/endpoints/api/team/addmember.go @@ -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) +} \ No newline at end of file diff --git a/app/http/endpoints/api/team/create.go b/app/http/endpoints/api/team/create.go new file mode 100644 index 0000000..6e6b0c8 --- /dev/null +++ b/app/http/endpoints/api/team/create.go @@ -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, + }) +} diff --git a/app/http/endpoints/api/team/delete.go b/app/http/endpoints/api/team/delete.go new file mode 100644 index 0000000..e10b525 --- /dev/null +++ b/app/http/endpoints/api/team/delete.go @@ -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) +} diff --git a/app/http/endpoints/api/team/getmembers.go b/app/http/endpoints/api/team/getmembers.go new file mode 100644 index 0000000..df802da --- /dev/null +++ b/app/http/endpoints/api/team/getmembers.go @@ -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 +} diff --git a/app/http/endpoints/api/team/getteams.go b/app/http/endpoints/api/team/getteams.go new file mode 100644 index 0000000..807979a --- /dev/null +++ b/app/http/endpoints/api/team/getteams.go @@ -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) +} diff --git a/app/http/endpoints/api/team/removemember.go b/app/http/endpoints/api/team/removemember.go new file mode 100644 index 0000000..741da56 --- /dev/null +++ b/app/http/endpoints/api/team/removemember.go @@ -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) +} \ No newline at end of file diff --git a/app/http/endpoints/api/team/types.go b/app/http/endpoints/api/team/types.go new file mode 100644 index 0000000..73088c8 --- /dev/null +++ b/app/http/endpoints/api/team/types.go @@ -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"` +} diff --git a/app/http/endpoints/manage/logsview.go b/app/http/endpoints/manage/logsview.go index 8837cbb..b5b54fb 100644 --- a/app/http/endpoints/manage/logsview.go +++ b/app/http/endpoints/manage/logsview.go @@ -61,7 +61,7 @@ func LogViewHandler(ctx *gin.Context) { // Verify the user has permissions to be here permLevel, err := utils.GetPermissionLevel(guildId, userId) if err != nil { - ctx.JSON(500, utils.ErrorToResponse(err)) + ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/middleware/authenticateguild.go b/app/http/middleware/authenticateguild.go index 955696e..9ae1354 100644 --- a/app/http/middleware/authenticateguild.go +++ b/app/http/middleware/authenticateguild.go @@ -49,7 +49,7 @@ func AuthenticateGuild(isApiMethod bool, requiredPermissionLevel permission.Perm permLevel, err := utils.GetPermissionLevel(guild.Id, userId) if err != nil { - ctx.AbortWithStatusJSON(500, utils.ErrorToResponse(err)) + ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/server.go b/app/http/server.go index 1669276..6208528 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -9,6 +9,7 @@ import ( 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_team "github.com/TicketsBot/GoPanel/app/http/endpoints/api/team" 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" @@ -89,6 +90,7 @@ func StartServer() { guildAuthApiSupport.GET("/premium", api.PremiumHandler) guildAuthApiSupport.GET("/user/:user", api.UserHandler) 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.POST("/settings", api_settings.UpdateSettingsHandler) @@ -123,6 +125,13 @@ func StartServer() { guildAuthApiAdmin.GET("/autoclose", api_autoclose.GetAutoClose) 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) diff --git a/botcontext/botcontext.go b/botcontext/botcontext.go index ec45b01..6745e45 100644 --- a/botcontext/botcontext.go +++ b/botcontext/botcontext.go @@ -10,6 +10,7 @@ import ( "github.com/rxdn/gdl/objects/channel" "github.com/rxdn/gdl/objects/guild" "github.com/rxdn/gdl/objects/member" + "github.com/rxdn/gdl/objects/user" "github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest/ratelimit" ) @@ -76,6 +77,15 @@ func (ctx BotContext) GetGuildMember(guildId, userId uint64) (m member.Member, e 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) { if roles := cache.Instance.GetGuildRoles(guildId); len(roles) > 0 { return roles, nil @@ -88,3 +98,17 @@ func (ctx BotContext) GetGuildRoles(guildId uint64) (roles []guild.Role, err err 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 +} diff --git a/go.mod b/go.mod index 1c17c57..0672c40 100644 --- a/go.mod +++ b/go.mod @@ -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-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/apex/log v1.1.2 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/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c 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/ulule/limiter/v3 v3.5.0 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a diff --git a/public/static/css/light-bootstrap-dashboard.css b/public/static/css/light-bootstrap-dashboard.css index dc1ea55..8ea4f4c 100644 --- a/public/static/css/light-bootstrap-dashboard.css +++ b/public/static/css/light-bootstrap-dashboard.css @@ -75,7 +75,6 @@ button.close { h1, .h1, h2, .h2, h3, .h3, h4, .h4 { font-weight: 300; - margin: 30px 0 15px; } h1, .h1 { diff --git a/public/static/css/style.css b/public/static/css/style.css index b61a8a9..2f0db36 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -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 { font-family: 'Noto Sans', sans-serif !important; - font-weight: 400 !important; } @@ -102,7 +101,6 @@ html > ::-webkit-scrollbar { } .table td, .table th { - text-align: center; } .close-container { @@ -309,7 +307,6 @@ html > ::-webkit-scrollbar { .flex-container { display: flex; - height: 100%; width: 100%; } @@ -331,14 +328,17 @@ html > ::-webkit-scrollbar { .tcontent-container { display: flex; justify-content: center; - height: 100%; width: 100%; } +.tcontent-container { + display: none; +} + .team-card-container { display: flex; - margin-top: 4%; - height: 50%; + margin: 4% 0; + min-height: 50%; width: 80%; } @@ -346,7 +346,6 @@ html > ::-webkit-scrollbar { display: flex; flex-direction: column; justify-content: space-evenly; - height: 100%; width: 100%; background-color: #272727 !important; @@ -371,7 +370,7 @@ html > ::-webkit-scrollbar { .tcard-body { display: flex; flex-direction: column; - margin: 2%; + padding: 2%; flex: 1; width: 100%; color: white; @@ -380,7 +379,120 @@ html > ::-webkit-scrollbar { .flex-center { display: flex; width: 100%; - height: 100%; flex-direction: column; 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; } \ No newline at end of file diff --git a/public/static/js/loadingscreen.js b/public/static/js/loadingscreen.js index 8f07dee..683d2e2 100644 --- a/public/static/js/loadingscreen.js +++ b/public/static/js/loadingscreen.js @@ -1,5 +1,5 @@ function showLoadingScreen() { - const content = document.getElementsByClassName('content')[0]; + const content = document.getElementsByClassName('content')[0] || document.getElementsByClassName('tcontent-container')[0]; content.style.display = 'none'; document.getElementById('loading-container').style.display = 'block'; } @@ -7,8 +7,13 @@ function showLoadingScreen() { function hideLoadingScreen() { document.getElementById('loading-container').style.display = 'none'; - const content = document.getElementsByClassName('content')[0]; - content.style.display = 'block'; + 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.classList.add('fade-in'); } diff --git a/public/static/js/utils.js b/public/static/js/utils.js index 3bb7939..f5f45b7 100644 --- a/public/static/js/utils.js +++ b/public/static/js/utils.js @@ -26,12 +26,12 @@ function appendTd(tr, content) { return td } -function appendButton(tr, content, onclick) { +function appendButton(tr, content, onclick, ...classList) { const tdRemove = document.createElement('td'); const btn = document.createElement('button'); 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.onclick = onclick; @@ -59,3 +59,9 @@ function prependChild(parent, child) { parent.insertBefore(child, parent.children[0]); } } + +function createElement(tag, ...classList) { + const el = document.createElement(tag); + el.classList.add(...classList); + return el; +} \ No newline at end of file diff --git a/public/templates/includes/notifymodal.tmpl b/public/templates/includes/notifymodal.tmpl index d6ce47f..96740db 100644 --- a/public/templates/includes/notifymodal.tmpl +++ b/public/templates/includes/notifymodal.tmpl @@ -45,6 +45,10 @@ notify('Success', message); } + function notifyRatelimit() { + notifyError("You're doing that too fast: please wait a few seconds and try again"); + } + function closeNotificationModal() { $('#notificationmodal').modal('hide'); } diff --git a/public/templates/includes/paneleditmodal.tmpl b/public/templates/includes/paneleditmodal.tmpl index 2792662..0f9054c 100644 --- a/public/templates/includes/paneleditmodal.tmpl +++ b/public/templates/includes/paneleditmodal.tmpl @@ -82,13 +82,20 @@