398 lines
9.8 KiB
Go
398 lines
9.8 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/TicketsBot/GoPanel/botcontext"
|
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
|
"github.com/TicketsBot/GoPanel/rpc"
|
|
"github.com/TicketsBot/GoPanel/rpc/cache"
|
|
"github.com/TicketsBot/GoPanel/utils"
|
|
"github.com/TicketsBot/common/premium"
|
|
"github.com/TicketsBot/database"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rxdn/gdl/objects/channel"
|
|
"github.com/rxdn/gdl/objects/channel/embed"
|
|
"github.com/rxdn/gdl/objects/guild/emoji"
|
|
"github.com/rxdn/gdl/objects/interaction/component"
|
|
"github.com/rxdn/gdl/rest"
|
|
"github.com/rxdn/gdl/rest/request"
|
|
"golang.org/x/sync/errgroup"
|
|
"regexp"
|
|
"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"`
|
|
WithDefaultTeam bool `json:"default_team"`
|
|
Teams []database.SupportTeam `json:"teams"`
|
|
ImageUrl *string `json:"image_url,omitempty"`
|
|
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
|
|
}
|
|
|
|
func CreatePanel(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 panelBody
|
|
|
|
if err := ctx.BindJSON(&data); err != nil {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
data.MessageId = 0
|
|
|
|
// Check panel quota
|
|
premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, false, botContext.Token, botContext.RateLimiter)
|
|
|
|
if premiumTier == premium.None {
|
|
panels, err := dbclient.Client.Panel.GetByGuild(guildId)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(500, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
if len(panels) >= freePanelLimit {
|
|
ctx.AbortWithStatusJSON(402, gin.H{
|
|
"success": false,
|
|
"error": "You have exceeded your panel quota. Purchase premium to unlock more panels.",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
if !data.doValidations(ctx, guildId) {
|
|
return
|
|
}
|
|
|
|
customId := utils.RandString(80)
|
|
|
|
emoji, _ := data.getEmoji() // already validated
|
|
msgId, err := data.sendEmbed(&botContext, data.Title, customId, emoji, data.ImageUrl, data.ThumbnailUrl, premiumTier > premium.None)
|
|
if err != nil {
|
|
var unwrapped request.RestError
|
|
if errors.As(err, &unwrapped) && unwrapped.StatusCode == 403 {
|
|
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
|
|
}
|
|
|
|
// 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,
|
|
WithDefaultTeam: data.WithDefaultTeam,
|
|
CustomId: customId,
|
|
ImageUrl: data.ImageUrl,
|
|
ThumbnailUrl: data.ThumbnailUrl,
|
|
}
|
|
|
|
panelId, err := dbclient.Client.Panel.Create(panel)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(500, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// insert role mention data
|
|
// 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(panelId, 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(panelId, roleId); err != nil {
|
|
ctx.AbortWithStatusJSON(500, gin.H{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if responseCode, err := insertTeams(guildId, panelId, data.Teams); err != nil {
|
|
ctx.JSON(responseCode, utils.ErrorJson(err))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(200, gin.H{
|
|
"success": true,
|
|
"panel_id": panelId,
|
|
})
|
|
}
|
|
|
|
// returns (response_code, error)
|
|
func insertTeams(guildId uint64, panelId int, teams []database.SupportTeam) (int, error) {
|
|
// insert teams
|
|
group, _ := errgroup.WithContext(context.Background())
|
|
for _, team := range teams {
|
|
group.Go(func() error {
|
|
// ensure team exists
|
|
exists, err := dbclient.Client.SupportTeam.Exists(team.Id, guildId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("team with id %d not found", team.Id)
|
|
}
|
|
|
|
return dbclient.Client.PanelTeams.Add(panelId, team.Id)
|
|
})
|
|
}
|
|
|
|
return 500, group.Wait()
|
|
}
|
|
|
|
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 {
|
|
if !p.verifyTitle() {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Panel titles must be between 1 - 80 characters in length",
|
|
})
|
|
return false
|
|
}
|
|
|
|
if !p.verifyContent() {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Panel content must be between 1 - 1024 characters in length",
|
|
})
|
|
return false
|
|
}
|
|
|
|
channels := cache.Instance.GetGuildChannels(guildId)
|
|
|
|
if !p.verifyChannel(channels) {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Invalid channel",
|
|
})
|
|
return false
|
|
}
|
|
|
|
if !p.verifyCategory(channels) {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Invalid channel category",
|
|
})
|
|
return false
|
|
}
|
|
|
|
_, validEmoji := p.getEmoji()
|
|
if !validEmoji {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Invalid emoji. Simply use the emoji's name from Discord.",
|
|
})
|
|
return false
|
|
}
|
|
|
|
if !p.verifyWelcomeMessage() {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Welcome message must be null or between 1 - 1024 characters",
|
|
})
|
|
return false
|
|
}
|
|
|
|
if !p.verifyImageUrl() || !p.verifyThumbnailUrl() {
|
|
ctx.AbortWithStatusJSON(400, gin.H{
|
|
"success": false,
|
|
"error": "Image URL must be between 1 - 255 characters and a valid URL",
|
|
})
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *panelBody) verifyTitle() bool {
|
|
if len(p.Title) > 80 {
|
|
return false
|
|
} else if len(p.Title) == 0 { // Fill default
|
|
p.Title = "Open a ticket!"
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *panelBody) verifyContent() bool {
|
|
if len(p.Content) > 1024 {
|
|
return false
|
|
} else if len(p.Content) == 0 { // Fill default
|
|
p.Content = "By clicking the button, a ticket will be opened for you."
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *panelBody) getEmoji() (emoji string, ok bool) {
|
|
p.Emote = strings.Replace(p.Emote, ":", "", -1)
|
|
|
|
emoji, ok = utils.GetEmoji(p.Emote)
|
|
return
|
|
}
|
|
|
|
func (p *panelBody) verifyChannel(channels []channel.Channel) bool {
|
|
var valid bool
|
|
for _, ch := range channels {
|
|
if ch.Id == p.ChannelId && ch.Type == channel.ChannelTypeGuildText {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return valid
|
|
}
|
|
|
|
func (p *panelBody) verifyCategory(channels []channel.Channel) bool {
|
|
var valid bool
|
|
for _, ch := range channels {
|
|
if ch.Id == p.CategoryId && ch.Type == channel.ChannelTypeGuildCategory {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return valid
|
|
}
|
|
|
|
func (p *panelBody) verifyWelcomeMessage() bool {
|
|
return p.WelcomeMessage == nil || (len(*p.WelcomeMessage) > 0 && len(*p.WelcomeMessage) < 1025)
|
|
}
|
|
|
|
func (p *panelBody) verifyImageUrl() bool {
|
|
if p.ImageUrl != nil && len(*p.ImageUrl) == 0 {
|
|
p.ImageUrl = nil
|
|
}
|
|
|
|
return p.ImageUrl == nil || (len(*p.ImageUrl) <= 255 && urlRegex.MatchString(*p.ImageUrl))
|
|
}
|
|
|
|
func (p *panelBody) verifyThumbnailUrl() bool {
|
|
if p.ThumbnailUrl != nil && len(*p.ThumbnailUrl) == 0 {
|
|
p.ThumbnailUrl = nil
|
|
}
|
|
|
|
return p.ThumbnailUrl == nil || (len(*p.ThumbnailUrl) <= 255 && urlRegex.MatchString(*p.ThumbnailUrl))
|
|
}
|
|
|
|
func (p *panelBody) sendEmbed(ctx *botcontext.BotContext, title, customId, emote string, imageUrl, thumbnailUrl *string, isPremium bool) (uint64, error) {
|
|
e := embed.NewEmbed().
|
|
SetTitle(p.Title).
|
|
SetDescription(p.Content).
|
|
SetColor(int(p.Colour))
|
|
|
|
if imageUrl != nil {
|
|
e.SetImage(*imageUrl)
|
|
}
|
|
|
|
if thumbnailUrl != nil {
|
|
e.SetThumbnail(*thumbnailUrl)
|
|
}
|
|
|
|
if !isPremium {
|
|
// TODO: Don't harcode
|
|
e.SetFooter("Powered by ticketsbot.net", "https://cdn.discordapp.com/avatars/508391840525975553/ac2647ffd4025009e2aa852f719a8027.png?size=256")
|
|
}
|
|
|
|
data := rest.CreateMessageData{
|
|
Embed: e,
|
|
Components: []component.Component{
|
|
{
|
|
Type: component.ComponentActionRow,
|
|
ComponentData: component.ActionRow{
|
|
Components: []component.Component{
|
|
{
|
|
Type: component.ComponentButton,
|
|
ComponentData: component.Button{
|
|
Label: title,
|
|
CustomId: customId,
|
|
Style: component.ButtonStylePrimary,
|
|
Emoji: emoji.Emoji{
|
|
Name: emote,
|
|
},
|
|
Url: nil,
|
|
Disabled: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
msg, err := rest.CreateMessage(ctx.Token, ctx.RateLimiter, p.ChannelId, data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return msg.Id, nil
|
|
}
|