diff --git a/app/http/endpoints/api/panelcreate.go b/app/http/endpoints/api/panelcreate.go index 483111a..0b8c56d 100644 --- a/app/http/endpoints/api/panelcreate.go +++ b/app/http/endpoints/api/panelcreate.go @@ -228,11 +228,11 @@ func (p *panel) verifyContent() bool { return len(p.Content) > 0 && len(p.Content) < 1025 } -func (p *panel) getEmoji() (string, bool) { +func (p *panel) getEmoji() (emoji string, ok bool) { p.Emote = strings.Replace(p.Emote, ":", "", -1) - emoji := utils.GetEmojiByName(p.Emote) - return emoji, emoji != "" + emoji, ok = utils.GetEmoji(p.Emote) + return } func (p *panel) verifyChannel(channels []channel.Channel) bool { diff --git a/app/http/endpoints/api/panelupdate.go b/app/http/endpoints/api/panelupdate.go new file mode 100644 index 0000000..1a1f580 --- /dev/null +++ b/app/http/endpoints/api/panelupdate.go @@ -0,0 +1,206 @@ +package api + +import ( + "github.com/TicketsBot/GoPanel/botcontext" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/rpc" + "github.com/TicketsBot/common/premium" + "github.com/TicketsBot/database" + "github.com/gin-gonic/gin" + "github.com/rxdn/gdl/rest" + "github.com/rxdn/gdl/rest/request" + "strconv" +) + +func UpdatePanel(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + + botContext, err := botcontext.ContextForGuild(guildId) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + var data panel + + if err := ctx.BindJSON(&data); err != nil { + ctx.AbortWithStatusJSON(400, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64) + if err != nil { + ctx.AbortWithStatusJSON(400, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + data.MessageId = messageId + + // get existing + existing, err := dbclient.Client.Panel.Get(data.MessageId) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // check guild ID matches + if existing.GuildId != guildId { + ctx.AbortWithStatusJSON(400, gin.H{ + "success": false, + "error": "Guild ID does not match", + }) + return + } + + if !data.doValidations(ctx, guildId) { + return + } + + // check if we need to update the message + shouldUpdateMessage := uint32(existing.Colour) != data.Colour || + existing.ChannelId != data.ChannelId || + existing.Content != data.Content || + existing.Title != data.Title || + existing.ReactionEmote != data.Emote + + emoji, _ := data.getEmoji() // already validated + newMessageId := messageId + + if shouldUpdateMessage { + // delete old message + if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, existing.ChannelId, existing.MessageId); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter) + newMessageId, err = data.sendEmbed(&botContext, premiumTier > premium.None) + if err != nil { + if err == request.ErrForbidden { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": "I do not have permission to send messages in the specified channel", + }) + } else { + // TODO: Most appropriate error? + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + } + + return + } + + // Add reaction + if err = rest.CreateReaction(botContext.Token, botContext.RateLimiter, data.ChannelId, newMessageId, emoji); err != nil { + if err == request.ErrForbidden { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": "I do not have permission to add reactions in the specified channel", + }) + } else { + // TODO: Most appropriate error? + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + } + + return + } + } + + // Store in DB + panel := database.Panel{ + MessageId: newMessageId, + ChannelId: data.ChannelId, + GuildId: guildId, + Title: data.Title, + Content: data.Content, + Colour: int32(data.Colour), + TargetCategory: data.CategoryId, + ReactionEmote: emoji, + WelcomeMessage: data.WelcomeMessage, + } + + if err = dbclient.Client.Panel.Update(messageId, panel); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // insert role mention data + // delete old data + if err = dbclient.Client.PanelRoleMentions.DeleteAll(newMessageId); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // TODO: Reduce to 1 query + if err = dbclient.Client.PanelUserMention.Set(newMessageId, false); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // string is role ID or "user" to mention the ticket opener + for _, mention := range data.Mentions { + if mention == "user" { + if err = dbclient.Client.PanelUserMention.Set(newMessageId, true); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + } else { + roleId, err := strconv.ParseUint(mention, 10, 64) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // should we check the role is a valid role in the guild? + // not too much of an issue if it isnt + + if err = dbclient.Client.PanelRoleMentions.Add(newMessageId, roleId); err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + } + } + + ctx.JSON(200, gin.H{ + "success": true, + "message_id": strconv.FormatUint(newMessageId, 10), + }) +} diff --git a/app/http/server.go b/app/http/server.go index 0cee33e..4081ed0 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -89,6 +89,7 @@ func StartServer() { guildAuthApi.GET("/panels", api.ListPanels) guildAuthApi.PUT("/panels", api.CreatePanel) + guildAuthApi.PUT("/panels/:message", api.UpdatePanel) guildAuthApi.DELETE("/panels/:message", api.DeletePanel) guildAuthApi.GET("/logs/", api.GetLogs) @@ -142,36 +143,43 @@ func createRenderer() multitemplate.Renderer { r = addManageTemplate(r, "blacklist") r = addManageTemplate(r, "logs") r = addManageTemplate(r, "modmaillogs") - r = addManageTemplate(r, "settings") + r = addManageTemplate(r, "settings", "./public/templates/includes/substitutionmodal.tmpl") r = addManageTemplate(r, "ticketlist") r = addManageTemplate(r, "ticketview") - r = addManageTemplate(r, "panels") + r = addManageTemplate(r, "panels", "./public/templates/includes/substitutionmodal.tmpl", "./public/templates/includes/paneleditmodal.tmpl") r = addManageTemplate(r, "tags") return r } -func addMainTemplate(renderer multitemplate.Renderer, name string) multitemplate.Renderer { - renderer.AddFromFiles(fmt.Sprintf("main/%s", name), +func addMainTemplate(renderer multitemplate.Renderer, name string, extra ...string) multitemplate.Renderer { + files := []string{ "./public/templates/layouts/main.tmpl", "./public/templates/includes/head.tmpl", "./public/templates/includes/sidebar.tmpl", "./public/templates/includes/loadingscreen.tmpl", fmt.Sprintf("./public/templates/views/%s.tmpl", name), - ) + } + + files = append(files, extra...) + + renderer.AddFromFiles(fmt.Sprintf("main/%s", name), files...) return renderer } -func addManageTemplate(renderer multitemplate.Renderer, name string) multitemplate.Renderer { - renderer.AddFromFiles(fmt.Sprintf("manage/%s", name), +func addManageTemplate(renderer multitemplate.Renderer, name string, extra ...string) multitemplate.Renderer { + files := []string{ "./public/templates/layouts/manage.tmpl", "./public/templates/includes/head.tmpl", "./public/templates/includes/sidebar.tmpl", "./public/templates/includes/navbar.tmpl", - "./public/templates/includes/substitutionmodal.tmpl", "./public/templates/includes/loadingscreen.tmpl", fmt.Sprintf("./public/templates/views/%s.tmpl", name), - ) + } + + files = append(files, extra...) + + renderer.AddFromFiles(fmt.Sprintf("manage/%s", name), files...) return renderer } diff --git a/go.mod b/go.mod index 0a313b0..f069d82 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-20200425115930-0ca198cc8306 github.com/TicketsBot/common v0.0.0-20200529141045-7426ad13f1a4 - github.com/TicketsBot/database v0.0.0-20200619194554-a6db672a94cf + github.com/TicketsBot/database v0.0.0-20200620140717-f747a0bb4238 github.com/apex/log v1.1.2 github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible diff --git a/public/static/css/style.css b/public/static/css/style.css index 83baeae..2af8984 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -195,3 +195,7 @@ html > ::-webkit-scrollbar { .bootstrap-select .bs-ok-default:after { color: #2ECC71 !important; } + +.wrapper { + z-index: 1000 !important; +} \ No newline at end of file diff --git a/public/static/js/modalbackdrop.js b/public/static/js/modalbackdrop.js new file mode 100644 index 0000000..84e93d5 --- /dev/null +++ b/public/static/js/modalbackdrop.js @@ -0,0 +1,23 @@ +function clear(...elements) { + for (const elementId of elements) { + document.getElementById(elementId).value = ''; + } +} + +function hideBackdrop() { + for (const backdrop of document.getElementsByClassName('modal-backdrop fade show')) { + backdrop.remove(); + } +} + +function registerHideListener(elementId) { + $(`#${elementId}`).on('hidden.bs.modal', hideBackdrop); +} + +function showBackdrop() { + hideBackdrop(); + + const backdrop = document.createElement('div'); + backdrop.classList.add('modal-backdrop', 'fade', 'show'); + document.getElementsByClassName('main-panel')[0].appendChild(backdrop); +} diff --git a/public/templates/includes/paneleditmodal.tmpl b/public/templates/includes/paneleditmodal.tmpl new file mode 100644 index 0000000..fa76cc1 --- /dev/null +++ b/public/templates/includes/paneleditmodal.tmpl @@ -0,0 +1,222 @@ +{{define "paneleditmodal"}} + + + +{{end}} \ No newline at end of file diff --git a/public/templates/includes/substitutionmodal.tmpl b/public/templates/includes/substitutionmodal.tmpl index fbf3ec4..7214a35 100644 --- a/public/templates/includes/substitutionmodal.tmpl +++ b/public/templates/includes/substitutionmodal.tmpl @@ -31,4 +31,13 @@ + + {{end}} \ No newline at end of file diff --git a/public/templates/layouts/manage.tmpl b/public/templates/layouts/manage.tmpl index a14492b..62c8a79 100644 --- a/public/templates/layouts/manage.tmpl +++ b/public/templates/layouts/manage.tmpl @@ -5,7 +5,6 @@
- {{template "substitutions" .}}
{{template "navbar" .}} {{template "loadingscreen" .}} diff --git a/public/templates/views/panels.tmpl b/public/templates/views/panels.tmpl index 19550aa..96da5b4 100644 --- a/public/templates/views/panels.tmpl +++ b/public/templates/views/panels.tmpl @@ -1,4 +1,8 @@ {{define "content"}} + + {{template "substitutions" .}} + {{template "paneleditmodal" .}} +
@@ -18,6 +22,7 @@ Panel Title Panel Content Ticket Channel Category + Edit Delete @@ -69,9 +74,6 @@
-
-
#
-
@@ -102,7 +104,7 @@
@@ -181,7 +183,7 @@ 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, welcome_message: welcomeMessage === '' ? null : welcomeMessage, - mentions: $('.selectpicker').val() + mentions: $('#mentions').val() }; const res = await axios.put('/api/{{.guildId}}/panels', data); @@ -194,8 +196,8 @@ } } - async function fillChannels(channels) { - const container = document.getElementById('channel-container'); + async function fillChannels(elementId, channels) { + const container = document.getElementById(elementId); channels.filter(ch => ch.type === 0).forEach(ch => { const el = document.createElement('option'); @@ -205,8 +207,8 @@ }); } - async function fillCategories(channels) { - const container = document.getElementById('category-container'); + async function fillCategories(elementId, channels) { + const container = document.getElementById(elementId); channels.filter(ch => ch.type === 4).forEach(ch => { const el = document.createElement('option'); @@ -242,6 +244,16 @@ appendTd(tr, panel.content); appendTd(tr, getChannelName(channels, panel.category_id)); + // build edit button + const editTd = document.createElement('td'); + const editButton = document.createElement('button'); + editButton.type = 'button'; + editButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto'); + editButton.appendChild(document.createTextNode('Edit')); + editButton.onclick = () => { openEditModal(panel.message_id) }; + editTd.appendChild(editButton); + tr.appendChild(editTd); + // build remove button const deleteTd = document.createElement('td'); const deleteButton = document.createElement('button'); @@ -269,8 +281,8 @@ return res.data.length; } - async function fillMentions() { - const select = document.getElementById('mentions'); + async function fillMentions(elementId) { + const select = document.getElementById(elementId); // ticket opener const ticketOpener = document.createElement('option'); @@ -294,7 +306,7 @@ } } - $('.selectpicker').selectpicker('refresh'); + $('#mentions').selectpicker('refresh'); } async function loadData() { @@ -302,13 +314,12 @@ const panelCount = await fillPanels(channels); await fillPanelQuota(panelCount); - await fillChannels(channels); - await fillCategories(channels); - await fillMentions(); + await fillChannels('channel-container', channels); + await fillCategories('category-container', channels); + await fillMentions('mentions'); } withLoadingScreen(loadData);
- {{end}} \ No newline at end of file diff --git a/public/templates/views/settings.tmpl b/public/templates/views/settings.tmpl index cac85a9..8a3f0fe 100644 --- a/public/templates/views/settings.tmpl +++ b/public/templates/views/settings.tmpl @@ -1,4 +1,7 @@ {{define "content"}} + + {{template "substitutions" .}} +
@@ -79,7 +82,7 @@
diff --git a/utils/emojiutil.go b/utils/emojiutil.go index 82377d6..844b310 100644 --- a/utils/emojiutil.go +++ b/utils/emojiutil.go @@ -6,7 +6,8 @@ import ( "io/ioutil" ) -var emojis map[string]interface{} +var emojisByName map[string]string +var emojis []string func LoadEmoji() { bytes, err := ioutil.ReadFile("emojis.json"); if err != nil { @@ -14,17 +15,31 @@ func LoadEmoji() { return } - if err := json.Unmarshal(bytes, &emojis); err != nil { + if err := json.Unmarshal(bytes, &emojisByName); err != nil { log.Error("Couldn't load emoji: " + err.Error()) return } + + emojis = make([]string, len(emojisByName)) + i := 0 + for _, emoji := range emojisByName { + emojis[i] = emoji + i++ + } } -func GetEmojiByName(name string) string { - emoji, ok := emojis[name]; if !ok { - return "" +func GetEmoji(input string) (emoji string, ok bool) { + // try by name first + emoji, ok = emojisByName[input] + if !ok { // else try by the actual unicode char + for _, unicode := range emojis { + if unicode == input { + emoji = unicode + ok = true + break + } + } } - str, _ := emoji.(string) - return str + return }