Custom emojis

This commit is contained in:
rxdn 2022-06-16 22:54:17 +01:00
parent 5d27ca2583
commit b8d2bf03ff
18 changed files with 412 additions and 169 deletions

View File

@ -2,9 +2,9 @@ package api
import ( import (
"github.com/TicketsBot/GoPanel/botcontext" "github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/TicketsBot/database" "github.com/TicketsBot/database"
"github.com/rxdn/gdl/objects/channel/embed" "github.com/rxdn/gdl/objects/channel/embed"
"github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction/component" "github.com/rxdn/gdl/objects/interaction/component"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/utils" "github.com/rxdn/gdl/utils"
@ -58,17 +58,12 @@ func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []databa
if d.SelectMenu { if d.SelectMenu {
options := make([]component.SelectOption, len(panels)) options := make([]component.SelectOption, len(panels))
for i, panel := range panels { for i, panel := range panels {
var emote *emoji.Emoji emoji := types.NewEmoji(panel.EmojiName, panel.EmojiId).IntoGdl()
if panel.ReactionEmote != "" {
emote = &emoji.Emoji{
Name: panel.ReactionEmote,
}
}
options[i] = component.SelectOption{ options[i] = component.SelectOption{
Label: panel.ButtonLabel, Label: panel.ButtonLabel,
Value: panel.CustomId, Value: panel.CustomId,
Emoji: emote, Emoji: emoji,
} }
} }
@ -88,18 +83,13 @@ func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []databa
} else { } else {
buttons := make([]component.Component, len(panels)) buttons := make([]component.Component, len(panels))
for i, panel := range panels { for i, panel := range panels {
var buttonEmoji *emoji.Emoji emoji := types.NewEmoji(panel.EmojiName, panel.EmojiId).IntoGdl()
if panel.ReactionEmote != "" {
buttonEmoji = &emoji.Emoji{
Name: panel.ReactionEmote,
}
}
buttons[i] = component.BuildButton(component.Button{ buttons[i] = component.BuildButton(component.Button{
Label: panel.ButtonLabel, Label: panel.ButtonLabel,
CustomId: panel.CustomId, CustomId: panel.CustomId,
Style: component.ButtonStyle(panel.ButtonStyle), Style: component.ButtonStyle(panel.ButtonStyle),
Emoji: buttonEmoji, Emoji: emoji,
}) })
} }

View File

@ -2,17 +2,18 @@ package api
import ( import (
"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"
"github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/TicketsBot/common/collections" "github.com/TicketsBot/common/collections"
"github.com/TicketsBot/common/premium" "github.com/TicketsBot/common/premium"
"github.com/TicketsBot/database" "github.com/TicketsBot/database"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel" "github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction/component" "github.com/rxdn/gdl/objects/interaction/component"
"github.com/rxdn/gdl/rest/request" "github.com/rxdn/gdl/rest/request"
"regexp" "regexp"
@ -29,7 +30,7 @@ type panelBody struct {
Content string `json:"content"` Content string `json:"content"`
Colour uint32 `json:"colour"` Colour uint32 `json:"colour"`
CategoryId uint64 `json:"category_id,string"` CategoryId uint64 `json:"category_id,string"`
Emote string `json:"emote"` Emoji types.Emoji `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"` WithDefaultTeam bool `json:"default_team"`
@ -42,11 +43,6 @@ type panelBody struct {
} }
func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData { func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData {
var emoji *string
if p.Emote != "" {
emoji = &p.Emote
}
return panelMessageData{ return panelMessageData{
ChannelId: p.ChannelId, ChannelId: p.ChannelId,
Title: p.Title, Title: p.Title,
@ -55,7 +51,7 @@ func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelM
Colour: int(p.Colour), Colour: int(p.Colour),
ImageUrl: p.ImageUrl, ImageUrl: p.ImageUrl,
ThumbnailUrl: p.ThumbnailUrl, ThumbnailUrl: p.ThumbnailUrl,
Emoji: emoji, Emoji: p.getEmoji(),
ButtonStyle: p.ButtonStyle, ButtonStyle: p.ButtonStyle,
ButtonLabel: p.ButtonLabel, ButtonLabel: p.ButtonLabel,
IsPremium: isPremium, IsPremium: isPremium,
@ -111,26 +107,33 @@ func CreatePanel(ctx *gin.Context) {
customId := utils.RandString(80) customId := utils.RandString(80)
emoji, _ := data.getEmoji() // already validated
data.Emote = emoji
messageData := data.IntoPanelMessageData(customId, premiumTier > premium.None) messageData := data.IntoPanelMessageData(customId, premiumTier > premium.None)
msgId, err := messageData.send(&botContext) msgId, err := messageData.send(&botContext)
if err != nil { if err != nil {
var unwrapped request.RestError var unwrapped request.RestError
if errors.As(err, &unwrapped) && unwrapped.StatusCode == 403 { if errors.As(err, &unwrapped) && unwrapped.StatusCode == 403 {
ctx.AbortWithStatusJSON(500, utils.ErrorStr("I do not have permission to send messages in the specified channel")) ctx.JSON(500, utils.ErrorStr("I do not have permission to send messages in the specified channel"))
} else { } else {
// TODO: Most appropriate error? // TODO: Most appropriate error?
ctx.AbortWithStatusJSON(500, gin.H{ ctx.JSON(500, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
} }
return return
} }
var emojiId *uint64
var emojiName *string
{
emoji := data.getEmoji()
if emoji != nil {
emojiName = &emoji.Name
if emoji.Id.Value != 0 {
emojiId = &emoji.Id.Value
}
}
}
// Store in DB // Store in DB
panel := database.Panel{ panel := database.Panel{
MessageId: msgId, MessageId: msgId,
@ -140,7 +143,8 @@ func CreatePanel(ctx *gin.Context) {
Content: data.Content, Content: data.Content,
Colour: int32(data.Colour), Colour: int32(data.Colour),
TargetCategory: data.CategoryId, TargetCategory: data.CategoryId,
ReactionEmote: emoji, EmojiId: emojiId,
EmojiName: emojiName,
WelcomeMessage: data.WelcomeMessage, WelcomeMessage: data.WelcomeMessage,
WithDefaultTeam: data.WithDefaultTeam, WithDefaultTeam: data.WithDefaultTeam,
CustomId: customId, CustomId: customId,
@ -208,6 +212,11 @@ func CreatePanel(ctx *gin.Context) {
var urlRegex = regexp.MustCompile(`^https?://([-a-zA-Z0-9@:%._+~#=]{1,256})\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$`) var urlRegex = regexp.MustCompile(`^https?://([-a-zA-Z0-9@:%._+~#=]{1,256})\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$`)
func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool { func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
return false // TODO: Log error
}
if !p.verifyTitle() { if !p.verifyTitle() {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.AbortWithStatusJSON(400, gin.H{
"success": false, "success": false,
@ -227,34 +236,22 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
channels := cache.Instance.GetGuildChannels(guildId) channels := cache.Instance.GetGuildChannels(guildId)
if !p.verifyChannel(channels) { if !p.verifyChannel(channels) {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, utils.ErrorStr("Invalid channel"))
"success": false,
"error": "Invalid channel",
})
return false return false
} }
if !p.verifyCategory(channels) { if !p.verifyCategory(channels) {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, utils.ErrorStr("Invalid channel category"))
"success": false,
"error": "Invalid channel category",
})
return false return false
} }
if p.Emote != "" { // Allow no emoji if !p.verifyEmoji(botContext, guildId) {
_, validEmoji := p.getEmoji() ctx.JSON(400, utils.ErrorStr("Invalid emoji"))
if !validEmoji { return false
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid emoji. Simply use the emoji itself, or the emoji's name from Discord.",
})
return false
}
} }
if !p.verifyWelcomeMessage() { if !p.verifyWelcomeMessage() {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, gin.H{
"success": false, "success": false,
"error": "Welcome message must be blank or between 1 - 4096 characters", "error": "Welcome message must be blank or between 1 - 4096 characters",
}) })
@ -262,7 +259,7 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
} }
if !p.verifyImageUrl() || !p.verifyThumbnailUrl() { if !p.verifyImageUrl() || !p.verifyThumbnailUrl() {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, gin.H{
"success": false, "success": false,
"error": "Image URL must be between 1 - 255 characters and a valid URL", "error": "Image URL must be between 1 - 255 characters and a valid URL",
}) })
@ -270,7 +267,7 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
} }
if !p.verifyButtonStyle() { if !p.verifyButtonStyle() {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, gin.H{
"success": false, "success": false,
"error": "Invalid button style", "error": "Invalid button style",
}) })
@ -278,20 +275,19 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
} }
if !p.verifyButtonLabel() { if !p.verifyButtonLabel() {
ctx.AbortWithStatusJSON(400, utils.ErrorStr("Button labels cannot be longer than 80 characters")) ctx.JSON(400, utils.ErrorStr("Button labels cannot be longer than 80 characters"))
return false return false
} }
fmt.Printf("label: %s\n", p.ButtonLabel)
{ {
valid, err := p.verifyTeams(guildId) valid, err := p.verifyTeams(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
return false return false
} }
if !valid { if !valid {
ctx.AbortWithStatusJSON(400, utils.ErrorStr("Invalid teams provided")) ctx.JSON(400, utils.ErrorStr("Invalid teams provided"))
return false return false
} }
} }
@ -299,12 +295,12 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
{ {
ok, err := p.verifyFormId(guildId) ok, err := p.verifyFormId(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
return false return false
} }
if !ok { if !ok {
ctx.AbortWithStatusJSON(400, utils.ErrorStr("Guild ID for form does not match")) ctx.JSON(400, utils.ErrorStr("Guild ID for form does not match"))
return false return false
} }
} }
@ -332,12 +328,9 @@ func (p *panelBody) verifyContent() bool {
return true return true
} }
func (p *panelBody) getEmoji() (emoji string, ok bool) { // Data must be validated before calling this function
p.Emote = strings.TrimSpace(p.Emote) func (p *panelBody) getEmoji() *emoji.Emoji {
p.Emote = strings.Replace(p.Emote, ":", "", -1) return p.Emoji.IntoGdl()
emoji, ok = utils.GetEmoji(p.Emote)
return
} }
func (p *panelBody) verifyChannel(channels []channel.Channel) bool { func (p *panelBody) verifyChannel(channels []channel.Channel) bool {
@ -364,6 +357,45 @@ func (p *panelBody) verifyCategory(channels []channel.Channel) bool {
return valid return valid
} }
func (p *panelBody) verifyEmoji(ctx botcontext.BotContext, guildId uint64) bool {
if p.Emoji.IsCustomEmoji {
if p.Emoji.Id == nil {
return false
}
emoji, err := ctx.GetGuildEmoji(guildId, *p.Emoji.Id)
if err != nil { // TODO: Log
return false
}
if emoji.Id.Value == 0 {
return false
}
if emoji.Name != p.Emoji.Name {
return false
}
return true
} else {
if len(p.Emoji.Name) == 0 {
return true
}
// Convert from :emoji: to unicode if we need to
name := strings.TrimSpace(p.Emoji.Name)
name = strings.Replace(name, ":", "", -1)
unicode, ok := utils.GetEmoji(name)
if !ok {
return false
}
p.Emoji.Name = unicode
return true
}
}
func (p *panelBody) verifyWelcomeMessage() bool { func (p *panelBody) verifyWelcomeMessage() bool {
return p.WelcomeMessage == nil || (len(*p.WelcomeMessage) > 0 && len(*p.WelcomeMessage) <= 4096) return p.WelcomeMessage == nil || (len(*p.WelcomeMessage) > 0 && len(*p.WelcomeMessage) <= 4096)
} }

View File

@ -1,8 +1,12 @@
package api package api
import ( import (
"fmt"
"github.com/TicketsBot/GoPanel/botcontext" "github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/premium"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
"strconv" "strconv"
@ -13,57 +17,89 @@ func DeletePanel(ctx *gin.Context) {
botContext, err := botcontext.ContextForGuild(guildId) botContext, err := botcontext.ContextForGuild(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{ ctx.JSON(500, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return return
} }
panelId, err := strconv.Atoi(ctx.Param("panelid")) panelId, err := strconv.Atoi(ctx.Param("panelid"))
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{ ctx.JSON(400, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return return
} }
panel, err := database.Client.Panel.GetById(panelId) panel, err := database.Client.Panel.GetById(panelId)
if err != nil { if err != nil {
ctx.JSON(500, gin.H{ ctx.JSON(500, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return return
} }
// verify panel belongs to guild // verify panel belongs to guild
if panel.GuildId != guildId { if panel.GuildId != guildId {
ctx.AbortWithStatusJSON(403, gin.H{ ctx.JSON(403, utils.ErrorStr("Guild ID doesn't match"))
"success": false,
"error": "Guild ID doesn't match",
})
return return
} }
if err := database.Client.Panel.Delete(panelId); err != nil { // Get any multi panels this panel is part of to use later
ctx.JSON(500, gin.H{ multiPanels, err := database.Client.MultiPanelTargets.GetMultiPanels(panelId)
"success": false, if err != nil {
"error": err.Error(), fmt.Println(err.Error())
}) ctx.JSON(500, utils.ErrorJson(err))
return
}
if err := database.Client.Panel.Delete(panelId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return return
} }
if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, panel.ChannelId, panel.MessageId); err != nil { if err := rest.DeleteMessage(botContext.Token, botContext.RateLimiter, panel.ChannelId, panel.MessageId); err != nil {
ctx.JSON(500, gin.H{ ctx.JSON(500, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return return
} }
ctx.JSON(200, gin.H{ // Get premium tier
"success": true, premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
}) if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Update all multi panels messages to remove the button
for i, multiPanel := range multiPanels {
// Only update 5 multi-panels maximum: Prevent DoS
if i >= 5 {
break
}
panels, err := database.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
messageData := multiPanelMessageData{
Title: multiPanel.Title,
Content: multiPanel.Content,
Colour: multiPanel.Colour,
ChannelId: multiPanel.ChannelId,
SelectMenu: multiPanel.SelectMenu,
IsPremium: premiumTier > premium.None,
}
messageId, err := messageData.send(&botContext, panels)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if err := database.Client.MultiPanels.UpdateMessageId(multiPanel.Id, messageId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Delete old panel
_ = rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId)
}
ctx.JSON(200, utils.SuccessResponse)
} }

View File

@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
dbclient "github.com/TicketsBot/GoPanel/database" dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/TicketsBot/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"
@ -12,9 +13,10 @@ import (
func ListPanels(ctx *gin.Context) { func ListPanels(ctx *gin.Context) {
type panelResponse struct { type panelResponse struct {
database.Panel database.Panel
Mentions []string `json:"mentions"` UseCustomEmoji bool `json:"use_custom_emoji"`
//Teams []database.SupportTeam `json:"teams"` Emoji types.Emoji `json:"emote"`
Teams []int `json:"teams"` Mentions []string `json:"mentions"`
Teams []int `json:"teams"`
} }
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
@ -72,9 +74,11 @@ func ListPanels(ctx *gin.Context) {
} }
wrapped[i] = panelResponse{ wrapped[i] = panelResponse{
Panel: p, Panel: p,
Mentions: mentions, UseCustomEmoji: p.EmojiId != nil,
Teams: teamIds, Emoji: types.NewEmoji(p.EmojiName, p.EmojiId),
Mentions: mentions,
Teams: teamIds,
} }
return nil return nil

View File

@ -3,6 +3,7 @@ package api
import ( import (
"github.com/TicketsBot/GoPanel/botcontext" "github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/database" "github.com/TicketsBot/database"
"github.com/rxdn/gdl/objects"
"github.com/rxdn/gdl/objects/channel/embed" "github.com/rxdn/gdl/objects/channel/embed"
"github.com/rxdn/gdl/objects/guild/emoji" "github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction/component" "github.com/rxdn/gdl/objects/interaction/component"
@ -12,18 +13,27 @@ import (
type panelMessageData struct { type panelMessageData struct {
ChannelId uint64 ChannelId uint64
Title, Content, CustomId string Title, Content, CustomId string
Colour int Colour int
ImageUrl, ThumbnailUrl, Emoji *string ImageUrl, ThumbnailUrl *string
ButtonStyle component.ButtonStyle Emoji *emoji.Emoji
ButtonLabel string ButtonStyle component.ButtonStyle
IsPremium bool ButtonLabel string
IsPremium bool
} }
func panelIntoMessageData(panel database.Panel, isPremium bool) panelMessageData { func panelIntoMessageData(panel database.Panel, isPremium bool) panelMessageData {
var emoji *string var emote *emoji.Emoji
if panel.ReactionEmote != "" { if panel.EmojiName != nil && *panel.EmojiName == "" { // No emoji = nil
emoji = &panel.ReactionEmote id := objects.NewNullSnowflake()
if panel.EmojiId != nil {
id = objects.NewNullableSnowflake(*panel.EmojiId)
}
emote = &emoji.Emoji{
Id: id,
Name: *panel.EmojiName,
}
} }
return panelMessageData{ return panelMessageData{
@ -34,7 +44,7 @@ func panelIntoMessageData(panel database.Panel, isPremium bool) panelMessageData
Colour: int(panel.Colour), Colour: int(panel.Colour),
ImageUrl: panel.ImageUrl, ImageUrl: panel.ImageUrl,
ThumbnailUrl: panel.ThumbnailUrl, ThumbnailUrl: panel.ThumbnailUrl,
Emoji: emoji, Emoji: emote,
ButtonStyle: component.ButtonStyle(panel.ButtonStyle), ButtonStyle: component.ButtonStyle(panel.ButtonStyle),
ButtonLabel: panel.ButtonLabel, ButtonLabel: panel.ButtonLabel,
IsPremium: isPremium, IsPremium: isPremium,
@ -59,13 +69,6 @@ func (p *panelMessageData) send(ctx *botcontext.BotContext) (uint64, error) {
e.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png") e.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png")
} }
var buttonEmoji *emoji.Emoji
if p.Emoji != nil {
buttonEmoji = &emoji.Emoji{
Name: *p.Emoji,
}
}
data := rest.CreateMessageData{ data := rest.CreateMessageData{
Embeds: []*embed.Embed{e}, Embeds: []*embed.Embed{e},
Components: []component.Component{ Components: []component.Component{
@ -73,7 +76,7 @@ func (p *panelMessageData) send(ctx *botcontext.BotContext) (uint64, error) {
Label: p.ButtonLabel, Label: p.ButtonLabel,
CustomId: p.CustomId, CustomId: p.CustomId,
Style: p.ButtonStyle, Style: p.ButtonStyle,
Emoji: buttonEmoji, Emoji: p.Emoji,
Url: nil, Url: nil,
Disabled: false, Disabled: false,
})), })),

View File

@ -53,49 +53,23 @@ func UpdatePanel(ctx *gin.Context) {
return return
} }
// check if this will break a multi-panel;
// first, get any multipanels this panel belongs to
multiPanels, err := dbclient.Client.MultiPanelTargets.GetMultiPanels(existing.PanelId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter) premiumTier, err := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
} }
for _, multiPanel := range multiPanels { var emojiId *uint64
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id) var emojiName *string
if err != nil { {
ctx.JSON(500, utils.ErrorJson(err)) emoji := data.getEmoji()
return if emoji != nil {
} emojiName = &emoji.Name
messageData := multiPanelMessageData{ if emoji.Id.Value != 0 {
Title: multiPanel.Title, emojiId = &emoji.Id.Value
Content: multiPanel.Content, }
Colour: multiPanel.Colour,
ChannelId: multiPanel.ChannelId,
SelectMenu: multiPanel.SelectMenu,
IsPremium: premiumTier > premium.None,
} }
messageId, err := messageData.send(&botContext, panels)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if err := dbclient.Client.MultiPanels.UpdateMessageId(multiPanel.Id, messageId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Delete old panel
_ = rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId)
} }
// check if we need to update the message // check if we need to update the message
@ -103,13 +77,13 @@ func UpdatePanel(ctx *gin.Context) {
existing.ChannelId != data.ChannelId || existing.ChannelId != data.ChannelId ||
existing.Content != data.Content || existing.Content != data.Content ||
existing.Title != data.Title || existing.Title != data.Title ||
existing.ReactionEmote != data.Emote || (existing.EmojiId == nil && emojiId != nil || existing.EmojiId != nil && emojiId == nil || (existing.EmojiId != nil && emojiId != nil && *existing.EmojiId != *emojiId)) ||
(existing.EmojiName == nil && emojiName != nil || existing.EmojiName != nil && emojiName == nil || (existing.EmojiName != nil && emojiName != nil && *existing.EmojiName != *emojiName)) ||
existing.ImageUrl != data.ImageUrl || existing.ImageUrl != data.ImageUrl ||
existing.ThumbnailUrl != data.ThumbnailUrl || existing.ThumbnailUrl != data.ThumbnailUrl ||
component.ButtonStyle(existing.ButtonStyle) != data.ButtonStyle || component.ButtonStyle(existing.ButtonStyle) != data.ButtonStyle ||
existing.ButtonLabel != data.ButtonLabel existing.ButtonLabel != data.ButtonLabel
emoji, _ := data.getEmoji() // already validated
newMessageId := existing.MessageId newMessageId := existing.MessageId
if shouldUpdateMessage { if shouldUpdateMessage {
@ -141,7 +115,8 @@ func UpdatePanel(ctx *gin.Context) {
Content: data.Content, Content: data.Content,
Colour: int32(data.Colour), Colour: int32(data.Colour),
TargetCategory: data.CategoryId, TargetCategory: data.CategoryId,
ReactionEmote: emoji, EmojiName: emojiName,
EmojiId: emojiId,
WelcomeMessage: data.WelcomeMessage, WelcomeMessage: data.WelcomeMessage,
WithDefaultTeam: data.WithDefaultTeam, WithDefaultTeam: data.WithDefaultTeam,
CustomId: existing.CustomId, CustomId: existing.CustomId,
@ -199,5 +174,51 @@ func UpdatePanel(ctx *gin.Context) {
return return
} }
// Update multi panels
// check if this will break a multi-panel;
// first, get any multipanels this panel belongs to
multiPanels, err := dbclient.Client.MultiPanelTargets.GetMultiPanels(existing.PanelId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
for i, multiPanel := range multiPanels {
// Only update 5 multi-panels maximum: Prevent DoS
if i >= 5 {
break
}
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
messageData := multiPanelMessageData{
Title: multiPanel.Title,
Content: multiPanel.Content,
Colour: multiPanel.Colour,
ChannelId: multiPanel.ChannelId,
SelectMenu: multiPanel.SelectMenu,
IsPremium: premiumTier > premium.None,
}
messageId, err := messageData.send(&botContext, panels)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if err := dbclient.Client.MultiPanels.UpdateMessageId(multiPanel.Id, messageId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Delete old panel
_ = rest.DeleteMessage(botContext.Token, botContext.RateLimiter, multiPanel.ChannelId, multiPanel.MessageId)
}
ctx.JSON(200, utils.SuccessResponse) ctx.JSON(200, utils.SuccessResponse)
} }

View File

@ -101,6 +101,19 @@ func (ctx BotContext) GetGuildRoles(guildId uint64) (roles []guild.Role, err err
return return
} }
func (ctx BotContext) GetGuildEmoji(guildId, emojiId uint64) (emoji.Emoji, error) {
if emoji, ok := cache.Instance.GetEmoji(guildId); ok {
return emoji, nil
}
emoji, err := rest.GetGuildEmoji(ctx.Token, ctx.RateLimiter, guildId, emojiId)
if err == nil {
go cache.Instance.StoreEmoji(emoji, guildId)
}
return emoji, err
}
func (ctx BotContext) GetGuildEmojis(guildId uint64) (emojis []emoji.Emoji, err error) { func (ctx BotContext) GetGuildEmojis(guildId uint64) (emojis []emoji.Emoji, err error) {
if emojis := cache.Instance.GetGuildEmojis(guildId); len(emojis) > 0 { if emojis := cache.Instance.GetGuildEmojis(guildId); len(emojis) > 0 {
return emojis, nil return emojis, nil

View File

@ -27,7 +27,8 @@
"rollup-plugin-livereload": "^2.0.0", "rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.48.0" "svelte": "^3.48.0",
"svelte-toggle": "^3.1.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -3169,6 +3170,12 @@
"resolved": "https://registry.npmjs.org/svelte-tabs/-/svelte-tabs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/svelte-tabs/-/svelte-tabs-1.1.0.tgz",
"integrity": "sha512-bCynxgET2uvqpB6xf/dVyqHjzmumRURQyh2QqXlrki8NxzO7h2WghF8qgpb5qeB5NTX1bMU+9Q5Hf5ey2WLaMg==" "integrity": "sha512-bCynxgET2uvqpB6xf/dVyqHjzmumRURQyh2QqXlrki8NxzO7h2WghF8qgpb5qeB5NTX1bMU+9Q5Hf5ey2WLaMg=="
}, },
"node_modules/svelte-toggle": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-toggle/-/svelte-toggle-3.1.0.tgz",
"integrity": "sha512-2gzDDMDhM+ImDaLEZVlnlHVY1340Y368tT4Qk5IwLnCeRJ4zV3cVwliVGacoHy7iCDukcGXzKwDzG/hTTcaljg==",
"dev": true
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz",
@ -5593,6 +5600,12 @@
"resolved": "https://registry.npmjs.org/svelte-tabs/-/svelte-tabs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/svelte-tabs/-/svelte-tabs-1.1.0.tgz",
"integrity": "sha512-bCynxgET2uvqpB6xf/dVyqHjzmumRURQyh2QqXlrki8NxzO7h2WghF8qgpb5qeB5NTX1bMU+9Q5Hf5ey2WLaMg==" "integrity": "sha512-bCynxgET2uvqpB6xf/dVyqHjzmumRURQyh2QqXlrki8NxzO7h2WghF8qgpb5qeB5NTX1bMU+9Q5Hf5ey2WLaMg=="
}, },
"svelte-toggle": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-toggle/-/svelte-toggle-3.1.0.tgz",
"integrity": "sha512-2gzDDMDhM+ImDaLEZVlnlHVY1340Y368tT4Qk5IwLnCeRJ4zV3cVwliVGacoHy7iCDukcGXzKwDzG/hTTcaljg==",
"dev": true
},
"terser": { "terser": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz",

View File

@ -20,7 +20,8 @@
"rollup-plugin-livereload": "^2.0.0", "rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.1.0", "rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.48.0" "svelte": "^3.48.0",
"svelte-toggle": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.4", "axios": "^0.21.4",

View File

@ -5,7 +5,9 @@
<div class="wrapper"> <div class="wrapper">
<input id="input" class="form-input" placeholder="{placeholder}" disabled="{disabled}" bind:value={value}> <input id="input" class="form-input" placeholder="{placeholder}" disabled="{disabled}" bind:value={value}>
{#if !disabled} {#if !disabled}
<EmojiSelector on:emoji={onUpdate}/> <div class="picker-wrapper">
<EmojiSelector on:emoji={onUpdate}/>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -49,9 +51,14 @@
border-left: none; border-left: none;
color: white; color: white;
z-index: 2; z-index: 2;
height: 100%;
} }
:global(.svelte-emoji-picker__trigger:active) { :global(.svelte-emoji-picker__trigger:active) {
background-color: #2e3136 !important; background-color: #2e3136 !important;
} }
.picker-wrapper {
max-height: 40px;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4} class="switch"> <div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4} class="switch">
<label for="input" class="form-label">{label}</label> <label for="input" class="form-label">{label}</label>
<input id="input" type="checkbox" bind:checked={value} on:change /> <input id="input" type="checkbox" bind:checked={value} on:change={() => console.log('b')} />
<span class="slider" /> <span class="slider" />
</div> </div>

View File

@ -33,7 +33,12 @@
<label for="emoji-pick-wrapper" class="form-label">Button Emoji</label> <label for="emoji-pick-wrapper" class="form-label">Button Emoji</label>
<div id="emoji-pick-wrapper" class="row"> <div id="emoji-pick-wrapper" class="row">
<div class="col-2"> <div class="col-2">
<Slider label="Custom Emoji" bind:value={data.use_custom_emoji} /> <label class="form-label" style="margin-bottom: 0 !important;">Custom Emoji</label>
<Toggle hideLabel
toggledColor="#66bb6a"
untoggledColor="#ccc"
bind:toggled={data.use_custom_emoji}
on:toggle={handleEmojiTypeChange} />
</div> </div>
{#if data.use_custom_emoji} {#if data.use_custom_emoji}
<!--bind:selectedValue={selectedMentions} <!--bind:selectedValue={selectedMentions}
@ -41,10 +46,12 @@
<div class="multiselect-super"> <div class="multiselect-super">
<Select items={emojis} <Select items={emojis}
Item={EmojiItem} Item={EmojiItem}
selectedValue={data.emote}
optionIdentifier="id" optionIdentifier="id"
getSelectionLabel={emojiNameMapper} getSelectionLabel={emojiNameMapper}
getOptionLabel={emojiNameMapper} getOptionLabel={emojiNameMapper}
placeholderAlwaysShow={true} /> placeholderAlwaysShow={true}
on:select={handleCustomEmojiChange} />
</div> </div>
{:else} {:else}
<EmojiInput col1=true bind:value={data.emote}/> <EmojiInput col1=true bind:value={data.emote}/>
@ -89,7 +96,7 @@
on:select={updateTeams} on:select={updateTeams}
isSearchable={false} isSearchable={false}
optionIdentifier="id" optionIdentifier="id"
getSelectionLabel={emojiNameMapper} getSelectionLabel={nameMapper}
getOptionLabel={nameMapper} getOptionLabel={nameMapper}
isMulti={true} /> isMulti={true} />
</div> </div>
@ -118,7 +125,7 @@
import Select from 'svelte-select'; import Select from 'svelte-select';
import Dropdown from "../form/Dropdown.svelte"; import Dropdown from "../form/Dropdown.svelte";
import Checkbox from "../form/Checkbox.svelte"; import Checkbox from "../form/Checkbox.svelte";
import Slider from "../form/Slider.svelte"; import Toggle from "svelte-toggle";
export let guildId; export let guildId;
export let seedDefault = true; export let seedDefault = true;
@ -194,6 +201,23 @@
} }
} }
function handleEmojiTypeChange(e) {
let isCustomEmoji = e.detail;
if (isCustomEmoji) {
data.emote = undefined;
} else {
data.emote = '📩';
}
}
function handleCustomEmojiChange(e) {
let emoji = e.detail;
data.emote = {
id: emoji.id,
name: emoji.name
};
}
function updateColour() { function updateColour() {
data.colour = colourToInt(tempColour); data.colour = colourToInt(tempColour);
} }
@ -223,6 +247,9 @@
.forEach((mention) => selectedMentions.push(mention)); .forEach((mention) => selectedMentions.push(mention));
} }
$: data.emote = data.emote;
console.log(data.emote)
tempColour = intToColour(data.colour); tempColour = intToColour(data.colour);
} }
@ -235,6 +262,7 @@
//title: 'Open a ticket!', //title: 'Open a ticket!',
//content: 'By clicking the button, a ticket will be opened for you.', //content: 'By clicking the button, a ticket will be opened for you.',
colour: 0x2ECC71, colour: 0x2ECC71,
use_custom_emoji: false,
emote: '📩', emote: '📩',
welcome_message: null, welcome_message: null,
mentions: [], mentions: [],

View File

@ -4,7 +4,7 @@
<span slot="title">Edit Panel</span> <span slot="title">Edit Panel</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<PanelCreationForm {guildId} {channels} {roles} {teams} {forms} bind:data={panel} seedDefault={false} /> <PanelCreationForm {guildId} {channels} {roles} {emojis} {teams} {forms} bind:data={panel} seedDefault={false} />
</div> </div>
<div slot="footer"> <div slot="footer">
@ -34,6 +34,7 @@
export let channels = []; export let channels = [];
export let forms = []; export let forms = [];
export let roles = []; export let roles = [];
export let emojis = [];
export let teams = []; export let teams = [];
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();

View File

@ -1,5 +1,5 @@
{#if editModal} {#if editModal}
<PanelEditModal {guildId} {channels} {roles} {teams} {forms} bind:panel={editData} <PanelEditModal {guildId} {channels} {roles} {emojis} {teams} {forms} bind:panel={editData}
on:close={() => editModal = false} on:confirm={submitEdit}/> on:close={() => editModal = false} on:confirm={submitEdit}/>
{/if} {/if}
@ -16,8 +16,6 @@
<div slot="body" class="card-body"> <div slot="body" class="card-body">
<p>Your panel quota: <b>{panels.length} / {isPremium ? '∞' : '3'}</b></p> <p>Your panel quota: <b>{panels.length} / {isPremium ? '∞' : '3'}</b></p>
<img src="https://cdn.discordapp.com/emojis/986995010136318022.png" />
<table style="margin-top: 10px"> <table style="margin-top: 10px">
<thead> <thead>
<tr> <tr>

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220609182514-8d43f86e8253 github.com/TicketsBot/common v0.0.0-20220609182514-8d43f86e8253
github.com/TicketsBot/database v0.0.0-20220616145240-1b6207291ca6 github.com/TicketsBot/database v0.0.0-20220616215313-0f5a33c3a2a6
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20220614162334-f81bf3f39aa5 github.com/TicketsBot/worker v0.0.0-20220614162334-f81bf3f39aa5
github.com/apex/log v1.1.2 github.com/apex/log v1.1.2

2
go.sum
View File

@ -7,6 +7,8 @@ github.com/TicketsBot/common v0.0.0-20220609182514-8d43f86e8253 h1:HbL0OBZHmU0Tb
github.com/TicketsBot/common v0.0.0-20220609182514-8d43f86e8253/go.mod h1:ZAoYcDD7SQLTsZT7dbo/X0J256+pogVRAReunCGng+U= github.com/TicketsBot/common v0.0.0-20220609182514-8d43f86e8253/go.mod h1:ZAoYcDD7SQLTsZT7dbo/X0J256+pogVRAReunCGng+U=
github.com/TicketsBot/database v0.0.0-20220616145240-1b6207291ca6 h1:lq5+CXNCQRUyd3ODq1Y6tUsEY7Y0e1YfLQr50C0Nuis= github.com/TicketsBot/database v0.0.0-20220616145240-1b6207291ca6 h1:lq5+CXNCQRUyd3ODq1Y6tUsEY7Y0e1YfLQr50C0Nuis=
github.com/TicketsBot/database v0.0.0-20220616145240-1b6207291ca6/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw= github.com/TicketsBot/database v0.0.0-20220616145240-1b6207291ca6/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/database v0.0.0-20220616215313-0f5a33c3a2a6 h1:DC9CoT5uMuIh0pYX69euWLa/0MZLZA4sA+umIhOr0qo=
github.com/TicketsBot/database v0.0.0-20220616215313-0f5a33c3a2a6/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=

View File

@ -10,7 +10,8 @@ var emojisByName map[string]string
var emojis []string var emojis []string
func LoadEmoji() { func LoadEmoji() {
bytes, err := ioutil.ReadFile("emojis.json"); if err != nil { bytes, err := ioutil.ReadFile("emojis.json")
if err != nil {
log.Error("Couldn't load emoji: " + err.Error()) log.Error("Couldn't load emoji: " + err.Error())
return return
} }
@ -32,7 +33,7 @@ func GetEmoji(input string) (emoji string, ok bool) {
// try by name first // try by name first
emoji, ok = emojisByName[input] emoji, ok = emojisByName[input]
if !ok { // else try by the actual unicode char if !ok { // else try by the actual unicode char
for _, unicode := range emojis { for _, unicode := range emojis { // TODO: Optimise
if unicode == input { if unicode == input {
emoji = unicode emoji = unicode
ok = true ok = true

93
utils/types/emoji.go Normal file
View File

@ -0,0 +1,93 @@
package types
import (
"encoding/json"
"fmt"
"github.com/rxdn/gdl/objects"
"github.com/rxdn/gdl/objects/guild/emoji"
)
type Emoji struct {
IsCustomEmoji bool
Name string
Id *uint64
}
func NewEmoji(emojiName *string, emojiId *uint64) Emoji {
if emojiName == nil || *emojiName == "" {
return Emoji{
IsCustomEmoji: false,
Name: "",
Id: nil,
}
}
return Emoji{
IsCustomEmoji: emojiId != nil,
Name: *emojiName,
Id: emojiId,
}
}
func (e Emoji) IntoGdl() *emoji.Emoji {
if e.IsCustomEmoji {
return &emoji.Emoji{
Id: objects.NewNullableSnowflake(*e.Id),
Name: e.Name,
}
} else {
if e.Name == "" {
return nil
} else {
return &emoji.Emoji{
Name: e.Name,
}
}
}
}
type customEmoji struct {
Name string `json:"name"`
Id *uint64 `json:"id,string"`
}
func (e *Emoji) UnmarshalJSON(data []byte) error {
var raw interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if raw == nil {
return fmt.Errorf("emoji data was nil")
}
switch v := raw.(type) {
case string:
e.IsCustomEmoji = false
e.Name = v
case map[string]interface{}:
var decoded customEmoji
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
e.IsCustomEmoji = true
e.Name = decoded.Name
e.Id = decoded.Id
default:
return fmt.Errorf("unknown type")
}
return nil
}
func (e Emoji) MarshalJSON() ([]byte, error) {
if e.IsCustomEmoji {
return json.Marshal(customEmoji{
Name: e.Name,
Id: e.Id,
})
} else {
return json.Marshal(e.Name)
}
}