finish rewrite

This commit is contained in:
Dot-Rar 2020-04-23 21:25:21 +01:00
parent 3e21f25dfd
commit 64c19c4dcf
60 changed files with 2292 additions and 1838 deletions

View File

@ -0,0 +1,33 @@
package api
import (
"fmt"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"strconv"
)
type userData struct {
Username string `json:"username"`
Discriminator string `json:"discriminator"`
}
func GetBlacklistHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
data := make(map[string]userData)
blacklistedUsers := table.GetBlacklistNodes(guildId)
for _, row := range blacklistedUsers {
formattedId := strconv.FormatUint(row.User, 10)
user, _ := cache.Instance.GetUser(row.User)
data[formattedId] = userData{
Username: user.Username,
Discriminator: fmt.Sprintf("%04d", user.Discriminator),
}
}
ctx.JSON(200, data)
}

View File

@ -0,0 +1,60 @@
package api
import (
"context"
"errors"
"fmt"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v4"
"strconv"
)
func AddBlacklistHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data userData
if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
parsedDiscrim, err := strconv.ParseInt(data.Discriminator, 10, 16)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
var targetId uint64
if err := cache.Instance.QueryRow(context.Background(), `select users.user_id from "users" where LOWER(users.data->>'Username')=LOWER($1) AND users.data->>'Discriminator'=$2;`, data.Username, strconv.FormatInt(parsedDiscrim, 10)).Scan(&targetId); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
ctx.AbortWithStatusJSON(404, gin.H{
"success": false,
"error": "user not found",
})
} else {
fmt.Println(err.Error())
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
}
return
}
// TODO: Don't blacklist staff or guild owner
go table.AddBlacklist(guildId, targetId)
ctx.JSON(200, gin.H{
"success": true,
"user_id": strconv.FormatUint(targetId, 10),
})
}

View File

@ -0,0 +1,26 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/gin-gonic/gin"
"strconv"
)
func RemoveBlacklistHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
userId, err := strconv.ParseUint(ctx.Param("user"), 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
go table.RemoveBlacklist(guildId, userId)
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -0,0 +1,23 @@
package api
import (
"encoding/json"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
)
func ChannelsHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
channels := cache.Instance.GetGuildChannels(guildId)
encoded, err := json.Marshal(channels)
if err != nil {
ctx.JSON(500, gin.H{
"success": true,
"error": err.Error(),
})
return
}
ctx.Data(200, gin.MIMEJSON, encoded)
}

View File

@ -0,0 +1,53 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/messagequeue"
"github.com/gin-gonic/gin"
)
type closeBody struct {
Reason string `json:"reason"`
}
func CloseTicket(ctx *gin.Context) {
userId := ctx.Keys["userid"].(uint64)
guildId := ctx.Keys["guildid"].(uint64)
uuid := ctx.Param("uuid")
var data closeBody
if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": true,
"error": "Missing reason",
})
return
}
// Verify that the ticket exists
ticketChan := make(chan table.Ticket)
go table.GetTicket(uuid, ticketChan)
ticket := <-ticketChan
if ticket.Uuid == "" {
ctx.AbortWithStatusJSON(404, gin.H{
"success": true,
"error": "Ticket does not exist",
})
return
}
if ticket.Guild != guildId {
ctx.AbortWithStatusJSON(403, gin.H{
"success": true,
"error": "Guild ID does not matched",
})
return
}
go messagequeue.Client.PublishTicketClose(ticket.Uuid, userId, data.Reason)
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -0,0 +1,76 @@
package api
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"regexp"
"strconv"
"strings"
)
var MentionRegex, _ = regexp.Compile("<@(\\d+)>")
func GetTicket(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
uuid := ctx.Param("uuid")
ticketChan := make(chan table.Ticket)
go table.GetTicket(uuid, ticketChan)
ticket := <-ticketChan
if ticket.Guild != guildId {
ctx.AbortWithStatusJSON(403, gin.H{
"success": false,
"error": "Guild ID doesn't match",
})
return
}
if !ticket.IsOpen {
ctx.AbortWithStatusJSON(404, gin.H{
"success": false,
"error": "Ticket does not exist",
})
return
}
// Get messages
messages, _ := rest.GetChannelMessages(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Channel, rest.GetChannelMessagesData{Limit: 100})
// Format messages, exclude unneeded data
messagesFormatted := make([]map[string]interface{}, 0)
for _, message := range utils.Reverse(messages) {
content := message.Content
// Format mentions properly
match := MentionRegex.FindAllStringSubmatch(content, -1)
for _, mention := range match {
if len(mention) >= 2 {
mentionedId, err := strconv.ParseUint(mention[1], 10, 64)
if err != nil {
continue
}
ch := make(chan string)
go table.GetUsername(mentionedId, ch)
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", <-ch))
}
}
messagesFormatted = append(messagesFormatted, map[string]interface{}{
"username": message.Author.Username,
"content": content,
})
}
ctx.JSON(200, gin.H{
"success": true,
"ticket": ticket,
"messages": messagesFormatted,
})
}

View File

@ -0,0 +1,49 @@
package api
import (
"fmt"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"strconv"
"strings"
)
func GetTickets(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
tickets := table.GetOpenTickets(guildId)
ticketsFormatted := make([]map[string]interface{}, 0)
for _, ticket := range tickets {
membersFormatted := make([]map[string]interface{}, 0)
for index, memberIdStr := range strings.Split(ticket.Members, ",") {
if memberId, err := strconv.ParseUint(memberIdStr, 10, 64); err == nil {
if memberId != 0 {
var separator string
if index != len(strings.Split(ticket.Members, ","))-1 {
separator = ", "
}
member, _ := cache.Instance.GetUser(memberId)
membersFormatted = append(membersFormatted, map[string]interface{}{
"username": member.Username,
"discrim": fmt.Sprintf("%04d", member.Discriminator),
"sep": separator,
})
}
}
}
owner, _ := cache.Instance.GetUser(ticket.Owner)
ticketsFormatted = append(ticketsFormatted, map[string]interface{}{
"uuid": ticket.Uuid,
"ticketId": ticket.TicketId,
"username": owner.Username,
"discrim": fmt.Sprintf("%04d", owner.Discriminator),
"members": membersFormatted,
})
}
ctx.JSON(200, ticketsFormatted)
}

View File

@ -0,0 +1,30 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/guild"
)
func GetGuilds(ctx *gin.Context) {
userId := ctx.Keys["userid"].(uint64)
userGuilds := table.GetGuilds(userId)
adminGuilds := make([]guild.Guild, 0)
for _, g := range userGuilds {
fakeGuild := guild.Guild{
Id: g.Id,
OwnerId: g.OwnerId,
Permissions: g.Permissions,
}
isAdmin := make(chan bool)
go utils.IsAdmin(fakeGuild, userId, isAdmin)
if <-isAdmin {
adminGuilds = append(adminGuilds, g)
}
}
ctx.JSON(200, adminGuilds)
}

View File

@ -0,0 +1,112 @@
package api
import (
"context"
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"strconv"
)
const (
pageLimit = 30
)
func GetLogs(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
page, err := strconv.Atoi(ctx.Param("page"))
if page < 1 {
page = 1
}
// Get ticket ID from URL
var ticketId int
if utils.IsInt(ctx.Query("ticketid")) {
ticketId, _ = strconv.Atoi(ctx.Query("ticketid"))
}
var tickets []table.Ticket
// Get tickets from DB
if ticketId > 0 {
ticketChan := make(chan table.Ticket)
go table.GetTicketById(guildId, ticketId, ticketChan)
ticket := <-ticketChan
if ticket.Uuid != "" && !ticket.IsOpen {
tickets = append(tickets, ticket)
}
} else {
// make slice of user IDs to filter by
filteredIds := make([]uint64, 0)
// Add userid param to slice
filteredUserId, _ := strconv.ParseUint(ctx.Query("userid"), 10, 64)
if filteredUserId != 0 {
filteredIds = append(filteredIds, filteredUserId)
}
// Get username from URL
if username := ctx.Query("username"); username != "" {
// username -> user id
rows, err := cache.Instance.PgCache.Query(context.Background(), `select users.user_id from users where LOWER("data"->>'Username') LIKE LOWER($1) and exists(SELECT FROM members where members.guild_id=$2);`, fmt.Sprintf("%%%s%%", username), guildId)
defer rows.Close()
if err != nil {
log.Error(err.Error())
return
}
for rows.Next() {
var filteredId uint64
if err := rows.Scan(&filteredId); err != nil {
continue
}
if filteredId != 0 {
filteredIds = append(filteredIds, filteredId)
}
}
}
if ctx.Query("userid") != "" || ctx.Query("username") != "" {
tickets = table.GetClosedTicketsByUserId(guildId, filteredIds)
} else {
tickets = table.GetClosedTickets(guildId)
}
}
// Select 30 logs + format them
formattedLogs := make([]map[string]interface{}, 0)
for i := (page - 1) * pageLimit; i < (page-1)*pageLimit+pageLimit; i++ {
if i >= len(tickets) {
break
}
ticket := tickets[i]
// get username
user, found := cache.Instance.GetUser(ticket.Owner)
if !found {
user, err = rest.GetUser(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Owner)
if err != nil {
log.Error(err.Error())
}
go cache.Instance.StoreUser(user)
}
formattedLogs = append(formattedLogs, map[string]interface{}{
"ticketid": ticket.TicketId,
"userid": strconv.FormatUint(ticket.Owner, 10),
"username": user.Username,
})
}
ctx.JSON(200, formattedLogs)
}

View File

@ -0,0 +1,135 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/messagequeue"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel"
)
func CreatePanel(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data panel
if err := ctx.BindJSON(&data); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
data.MessageId = 0
// Check panel quota
premium := make(chan bool)
go utils.IsPremiumGuild(guildId, premium)
if !<-premium {
panels := make(chan []table.Panel)
go table.GetPanelsByGuild(guildId, panels)
if len(<-panels) > 0 {
ctx.AbortWithStatusJSON(402, gin.H{
"success": false,
"error": "You have exceeded your panel quota. Purchase premium to unlock more panels.",
})
return
}
}
if !data.verifyTitle() {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Panel titles must be between 1 - 255 characters in length",
})
return
}
if !data.verifyContent() {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Panel content must be between 1 - 1024 characters in length",
})
return
}
channels := cache.Instance.GetGuildChannels(guildId)
if !data.verifyChannel(channels) {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid channel",
})
return
}
if !data.verifyCategory(channels) {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid channel category",
})
return
}
emoji, validEmoji := data.getEmoji()
if !validEmoji {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid emoji. Simply use the emoji's name from Discord.",
})
return
}
// TODO: Move panel create logic here
go messagequeue.Client.PublishPanelCreate(table.Panel{
ChannelId: data.ChannelId,
GuildId: guildId,
Title: data.Title,
Content: data.Content,
Colour: data.Colour,
TargetCategory: data.CategoryId,
ReactionEmote: emoji,
})
ctx.JSON(200, gin.H{
"success": true,
})
}
func (p *panel) verifyTitle() bool {
return len(p.Title) > 0 && len(p.Title) < 256
}
func (p *panel) verifyContent() bool {
return len(p.Content) > 0 && len(p.Content) < 1025
}
func (p *panel) getEmoji() (string, bool) {
emoji := utils.GetEmojiByName(p.Emote)
return emoji, emoji != ""
}
func (p *panel) 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 *panel) 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
}

View File

@ -0,0 +1,43 @@
package api
import (
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"strconv"
)
func DeletePanel(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
messageId, err := strconv.ParseUint(ctx.Param("message"), 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// verify panel belongs to guild
panelChan := make(chan table.Panel)
go table.GetPanel(messageId, panelChan)
panel := <-panelChan
if panel.GuildId != guildId {
ctx.AbortWithStatusJSON(403, gin.H{
"success": false,
"error": "Guild ID doesn't match",
})
return
}
go table.DeletePanel(messageId)
go rest.DeleteMessage(config.Conf.Bot.Token, ratelimit.Ratelimiter, panel.ChannelId, panel.MessageId)
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -0,0 +1,40 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/gin-gonic/gin"
)
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"`
}
func ListPanels(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
panelsChan := make(chan []table.Panel)
go table.GetPanelsByGuild(guildId, panelsChan)
panels := <-panelsChan
wrapped := make([]panel, len(panels))
for i, p := range panels {
wrapped[i] = panel{
ChannelId: p.ChannelId,
MessageId: p.MessageId,
Title: p.Title,
Content: p.Content,
Colour: p.Colour,
CategoryId: p.TargetCategory,
Emote: p.ReactionEmote,
}
}
ctx.JSON(200, wrapped)
}

View File

@ -0,0 +1,17 @@
package api
import (
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
)
func PremiumHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
isPremium := make(chan bool)
go utils.IsPremiumGuild(guildId, isPremium)
ctx.JSON(200, gin.H{
"premium": <-isPremium,
})
}

View File

@ -0,0 +1,132 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"net/http"
"time"
)
type sendMessageBody struct {
Message string `json:"message"`
}
func SendMessage(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
userId := ctx.Keys["userid"].(uint64)
var body sendMessageBody
if err := ctx.BindJSON(&body); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Message is missing",
})
return
}
// Verify guild is premium
isPremium := make(chan bool)
go utils.IsPremiumGuild(guildId, isPremium)
if !<-isPremium {
ctx.AbortWithStatusJSON(402, gin.H{
"success": false,
"error": "Guild is not premium",
})
return
}
// Get ticket
ticketChan := make(chan table.Ticket)
go table.GetTicket(ctx.Param("uuid"), ticketChan)
ticket := <-ticketChan
// Verify the ticket exists
if ticket.TicketId == 0 {
ctx.AbortWithStatusJSON(404, gin.H{
"success": false,
"error": "Ticket not found",
})
return
}
// Verify the user has permission to send to this guild
if ticket.Guild != guildId {
ctx.AbortWithStatusJSON(403, gin.H{
"success": false,
"error": "Guild ID doesn't match",
})
return
}
user, _ := cache.Instance.GetUser(userId)
if len(body.Message) > 2000 {
body.Message = body.Message[0:1999]
}
// Preferably send via a webhook
webhookChan := make(chan *string)
go table.GetWebhookByUuid(ticket.Uuid, webhookChan)
webhook := <-webhookChan
success := false
if webhook != nil {
// TODO: Use gdl execute webhook wrapper
success = executeWebhook(ticket.Uuid, *webhook, body.Message, user.Username, user.AvatarUrl(256))
}
if !success {
body.Message = fmt.Sprintf("**%s**: %s", user.Username, body.Message)
if len(body.Message) > 2000 {
body.Message = body.Message[0:1999]
}
_, _ = rest.CreateMessage(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Channel, rest.CreateMessageData{Content: body.Message})
}
ctx.JSON(200, gin.H{
"success": true,
})
}
func executeWebhook(uuid, webhook, content, username, avatar string) bool {
body := map[string]interface{}{
"content": content,
"username": username,
"avatar_url": avatar,
}
encoded, err := json.Marshal(&body)
if err != nil {
return false
}
req, err := http.NewRequest("POST", fmt.Sprintf("https://canary.discordapp.com/api/webhooks/%s", webhook), bytes.NewBuffer(encoded))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
client.Timeout = 3 * time.Second
res, err := client.Do(req)
if err != nil {
return false
}
if res.StatusCode == 404 || res.StatusCode == 403 {
go table.DeleteWebhookByUuid(uuid)
} else {
return true
}
return false
}

View File

@ -0,0 +1,56 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/gin-gonic/gin"
)
type Settings struct {
Prefix string `json:"prefix"`
WelcomeMessaage string `json:"welcome_message"`
TicketLimit int `json:"ticket_limit"`
Category uint64 `json:"category,string"`
ArchiveChannel uint64 `json:"archive_channel,string"`
NamingScheme table.NamingScheme `json:"naming_scheme"`
PingEveryone bool `json:"ping_everyone"`
UsersCanClose bool `json:"users_can_close"`
}
func GetSettingsHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
prefix := make(chan string)
go table.GetPrefix(guildId, prefix)
welcomeMessage := make(chan string)
go table.GetWelcomeMessage(guildId, welcomeMessage)
ticketLimit := make(chan int)
go table.GetTicketLimit(guildId, ticketLimit)
category := make(chan uint64)
go table.GetChannelCategory(guildId, category)
archiveChannel := make(chan uint64)
go table.GetArchiveChannel(guildId, archiveChannel)
allowUsersToClose := make(chan bool)
go table.IsUserCanClose(guildId, allowUsersToClose)
namingScheme := make(chan table.NamingScheme)
go table.GetTicketNamingScheme(guildId, namingScheme)
pingEveryone := make(chan bool)
go table.GetPingEveryone(guildId, pingEveryone)
ctx.JSON(200, Settings{
Prefix: <-prefix,
WelcomeMessaage: <-welcomeMessage,
TicketLimit: <-ticketLimit,
Category: <-category,
ArchiveChannel: <-archiveChannel,
NamingScheme: <-namingScheme,
PingEveryone: <-pingEveryone,
UsersCanClose: <-allowUsersToClose,
})
}

View File

@ -0,0 +1,34 @@
package api
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/utils"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func TokenHandler(ctx *gin.Context) {
session := sessions.Default(ctx)
userId := utils.GetUserId(session)
//TODO : CSRF
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userid": strconv.FormatUint(userId, 10),
})
str, err := token.SignedString([]byte(config.Conf.Server.Secret))
if err != nil {
fmt.Println(err.Error())
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
} else {
ctx.JSON(200, gin.H{
"success": true,
"token": str,
})
}
}

View File

@ -0,0 +1,130 @@
package api
import (
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel"
)
func UpdateSettingsHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var settings Settings
if err := ctx.BindJSON(&settings); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// Get a list of all channel IDs
channels := cache.Instance.GetGuildChannels(guildId)
// Get prefix
validPrefix := settings.updatePrefix(guildId)
validWelcomeMessage := settings.updateWelcomeMessage(guildId)
validTicketLimit := settings.updateTicketLimit(guildId)
validArchiveChannel := settings.updateArchiveChannel(channels, guildId)
validCategory := settings.updateCategory(channels, guildId)
validNamingScheme := settings.updateNamingScheme(guildId)
settings.updatePingEveryone(guildId)
settings.updateUsersCanClose(guildId)
ctx.JSON(200, gin.H{
"prefix": validPrefix,
"welcome_message": validWelcomeMessage,
"ticket_limit": validTicketLimit,
"archive_channel": validArchiveChannel,
"category": validCategory,
"naming_scheme": validNamingScheme,
})
}
func (s *Settings) updatePrefix(guildId uint64) bool {
if s.Prefix == "" || len(s.Prefix) > 8 {
return false
}
go table.UpdatePrefix(guildId, s.Prefix)
return true
}
func (s *Settings) updateWelcomeMessage(guildId uint64) bool {
if s.WelcomeMessaage == "" || len(s.WelcomeMessaage) > 1000 {
return false
}
go table.UpdateWelcomeMessage(guildId, s.WelcomeMessaage)
return true
}
func (s *Settings) updateTicketLimit(guildId uint64) bool {
if s.TicketLimit > 10 || s.TicketLimit < 1 {
return false
}
go table.UpdateTicketLimit(guildId, s.TicketLimit)
return true
}
func (s *Settings) updateCategory(channels []channel.Channel, guildId uint64) bool {
var valid bool
for _, ch := range channels {
if ch.Id == s.Category && ch.Type == channel.ChannelTypeGuildCategory {
valid = true
break
}
}
if !valid {
return false
}
go table.UpdateChannelCategory(guildId, s.Category)
return true
}
func (s *Settings) updateArchiveChannel(channels []channel.Channel, guildId uint64) bool {
var valid bool
for _, ch := range channels {
if ch.Id == s.ArchiveChannel && ch.Type == channel.ChannelTypeGuildText {
valid = true
break
}
}
if !valid {
return false
}
go table.UpdateArchiveChannel(guildId, s.ArchiveChannel)
return true
}
var validScheme = []table.NamingScheme{table.Id, table.Username}
func (s *Settings) updateNamingScheme(guildId uint64) bool {
var valid bool
for _, scheme := range validScheme {
if scheme == s.NamingScheme {
valid = true
break
}
}
if !valid {
return false
}
go table.SetTicketNamingScheme(guildId, s.NamingScheme)
return true
}
func (s *Settings) updatePingEveryone(guildId uint64) {
go table.UpdatePingEveryone(guildId, s.PingEveryone)
}
func (s *Settings) updateUsersCanClose(guildId uint64) {
go table.SetUserCanClose(guildId, s.UsersCanClose)
}

View File

@ -2,100 +2,18 @@ package manage
import ( import (
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv"
) )
func BlacklistHandler(ctx *gin.Context) { func BlacklistHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil { guildId := ctx.Keys["guildid"].(uint64)
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
blacklistedUsers := table.GetBlacklistNodes(guildId)
var blacklistedIds []uint64
for _, user := range blacklistedUsers {
blacklistedIds = append(blacklistedIds, user.User)
}
nodes := table.GetUserNodes(blacklistedIds)
var blacklisted []map[string]interface{}
for _, node := range nodes {
blacklisted = append(blacklisted, map[string]interface{}{
"userId": node.Id,
"username": utils.Base64Decode(node.Name),
"discrim": node.Discriminator,
})
}
userNotFound := false
isStaff := false
if store.Get("csrf").(string) == ctx.Query("csrf") { // CSRF is correct *and* set
username := ctx.Query("username")
discrim := ctx.Query("discrim")
// Verify that the user ID is real and in a shared guild
targetId := table.GetUserId(username, discrim)
exists := targetId != 0
if exists {
if guild.OwnerId == targetId || table.IsStaff(guildId, targetId) { // Prevent users from blacklisting staff
isStaff = true
} else {
if !utils.Contains(blacklistedIds, targetId) { // Prevent duplicates
table.AddBlacklist(guildId, targetId)
blacklisted = append(blacklisted, map[string]interface{}{
"userId": targetId,
"username": username,
"discrim": discrim,
})
}
}
} else {
userNotFound = true
}
}
ctx.HTML(200, "manage/blacklist", gin.H{ ctx.HTML(200, "manage/blacklist", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildId,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl, "baseUrl": config.Conf.Server.BaseUrl,
"blacklisted": blacklisted,
"userNotFound": userNotFound,
"isStaff": isStaff,
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -1,56 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func BlacklistRemoveHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
if ctx.Query("c") == store.Get("csrf").(string) {
targetIdStr := ctx.Param("user")
targetId, err := strconv.ParseUint(targetIdStr, 10, 64)
if err == nil { // If it's a real ID
table.RemoveBlacklist(guildId, targetId)
}
}
ctx.Redirect(302, fmt.Sprintf("/manage/%s/blacklist", guildIdStr))
} else {
ctx.Redirect(302, "/login")
}
}

View File

@ -1,154 +1,19 @@
package manage package manage
import ( import (
"context"
"fmt"
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/apex/log"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"strconv"
) )
func LogsHandler(ctx *gin.Context) { func LogsHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil { guildId := ctx.Keys["guildid"].(uint64)
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
pageStr := ctx.Param("page")
page := 1
i, err := strconv.Atoi(pageStr)
if err == nil {
if i > 0 {
page = i
}
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
pageLimit := 30
// Get ticket ID from URL
var ticketId int
if utils.IsInt(ctx.Query("ticketid")) {
ticketId, _ = strconv.Atoi(ctx.Query("ticketid"))
}
var tickets []table.Ticket
// Get tickets from DB
if ticketId > 0 {
ticketChan := make(chan table.Ticket)
go table.GetTicketById(guildId, ticketId, ticketChan)
ticket := <-ticketChan
if ticket.Uuid != "" && !ticket.IsOpen {
tickets = append(tickets, ticket)
}
} else {
// make slice of user IDs to filter by
filteredIds := make([]uint64, 0)
// Add userid param to slice
filteredUserId, _ := strconv.ParseUint(ctx.Query("userid"), 10, 64)
if filteredUserId != 0 {
filteredIds = append(filteredIds, filteredUserId)
}
// Get username from URL
if username := ctx.Query("username"); username != "" {
// username -> user id
rows, err := cache.Instance.PgCache.Query(context.Background(), `select users.user_id from users where LOWER("data"->>'Username') LIKE LOWER($1) and exists(SELECT FROM members where members.guild_id=$2);`, fmt.Sprintf("%%%s%%", username), guildId)
defer rows.Close()
if err != nil {
log.Error(err.Error())
return
}
for rows.Next() {
var filteredId uint64
if err := rows.Scan(&filteredId); err != nil {
continue
}
if filteredId != 0 {
filteredIds = append(filteredIds, filteredId)
}
}
}
if ctx.Query("userid") != "" || ctx.Query("username") != "" {
tickets = table.GetClosedTicketsByUserId(guildId, filteredIds)
} else {
tickets = table.GetClosedTickets(guildId)
}
}
// Select 30 logs + format them
var formattedLogs []map[string]interface{}
for i := (page - 1) * pageLimit; i < (page - 1) * pageLimit + pageLimit; i++ {
if i >= len(tickets) {
break
}
ticket := tickets[i]
// get username
user, found := cache.Instance.GetUser(ticket.Owner)
if !found {
user, err = rest.GetUser(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Owner)
if err != nil {
log.Error(err.Error())
}
go cache.Instance.StoreUser(user)
}
formattedLogs = append(formattedLogs, map[string]interface{}{
"ticketid": ticket.TicketId,
"userid": ticket.Owner,
"username": user.Username,
})
}
ctx.HTML(200, "manage/logs", gin.H{ ctx.HTML(200, "manage/logs", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildId,
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl, "baseUrl": config.Conf.Server.BaseUrl,
"isPageOne": page == 1,
"previousPage": page - 1,
"nextPage": page + 1,
"logs": formattedLogs,
"page": page,
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -20,7 +20,6 @@ func LogViewHandler(ctx *gin.Context) {
if store == nil { if store == nil {
return return
} }
defer store.Save()
if utils.IsLoggedIn(store) { if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store) userId := utils.GetUserId(store)
@ -37,7 +36,7 @@ func LogViewHandler(ctx *gin.Context) {
// format ticket ID // format ticket ID
ticketId, err := strconv.Atoi(ctx.Param("ticket")); if err != nil { ticketId, err := strconv.Atoi(ctx.Param("ticket")); if err != nil {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/page/1", guild.Id)) ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs", guild.Id))
return return
} }
@ -48,7 +47,7 @@ func LogViewHandler(ctx *gin.Context) {
// Verify this is a valid ticket and it is closed // Verify this is a valid ticket and it is closed
if ticket.Uuid == "" || ticket.IsOpen { if ticket.Uuid == "" || ticket.IsOpen {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/page/1", guild.Id)) ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs", guild.Id))
return return
} }

View File

@ -1,175 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/messagequeue"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel"
"strconv"
"strings"
)
func PanelCreateHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get CSRF token
csrfCorrect := ctx.PostForm("csrf") == store.Get("csrf").(string)
if !csrfCorrect {
ctx.Redirect(302, "/")
return
}
// Get if the guild is premium
premiumChan := make(chan bool)
go utils.IsPremiumGuild(store, guildId, premiumChan)
premium := <-premiumChan
// Check the user hasn't met their panel quota
if !premium {
panels := make(chan []table.Panel)
go table.GetPanelsByGuild(guildId, panels)
if len(<-panels) > 1 {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?metQuota=true", guildId))
return
}
}
// Validate title
title := ctx.PostForm("title")
if len(title) == 0 || len(title) > 255 {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validTitle=false", guildId))
return
}
// Validate content
content := ctx.PostForm("content")
if len(content) == 0 || len(content) > 1024 {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validContent=false", guildId))
return
}
// Validate colour
validColour := true
panelColourHex := strings.Replace(ctx.PostForm("colour"), "#", "", -1)
panelColour, err := strconv.ParseUint(panelColourHex, 16, 32)
if err != nil {
validColour = false
panelColour = 0x23A31A
}
// Validate channel
channelIdStr := ctx.PostForm("channel")
channelId, err := strconv.ParseUint(channelIdStr, 10, 64); if err != nil {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validChannel=false", guildId))
return
}
validChannel := make(chan bool)
go validateChannel(guildId, channelId, validChannel)
if !<-validChannel {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validChannel=false", guildId))
return
}
// Validate category
categoryStr := ctx.PostForm("categories")
categoryId, err := strconv.ParseUint(categoryStr, 10, 64); if err != nil {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validCategory=false", guildId))
return
}
validCategory := make(chan bool)
go validateCategory(guildId, categoryId, validCategory)
if !<-validCategory {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validCategory=false", guildId))
return
}
// Validate reaction emote
reaction := strings.ToLower(ctx.PostForm("reaction"))
if len(reaction) == 0 || len(reaction) > 32 {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validReaction=false", guildId))
return
}
reaction = strings.Replace(reaction, ":", "", -1)
emoji := utils.GetEmojiByName(reaction)
if emoji == "" {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?validReaction=false", guildId))
return
}
settings := table.Panel{
ChannelId: channelId,
GuildId: guildId,
Title: title,
Content: content,
Colour: int(panelColour),
TargetCategory: categoryId,
ReactionEmote: emoji,
}
go messagequeue.Client.PublishPanelCreate(settings)
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels?created=true&validColour=%t", guildId, validColour))
} else {
ctx.Redirect(302, "/login")
}
}
func validateChannel(guildId, channelId uint64, res chan bool) {
// Compare channel IDs
validChannel := false
for _, guildChannel := range cache.Instance.GetGuildChannels(guildId) {
if guildChannel.Id == channelId {
validChannel = true
break
}
}
res <- validChannel
}
func validateCategory(guildId, categoryId uint64, res chan bool) {
// Compare ch IDs
validCategory := false
for _, ch := range cache.Instance.GetGuildChannels(guildId) {
if ch.Type == channel.ChannelTypeGuildCategory && ch.Id == categoryId {
validCategory = true
break
}
}
res <- validCategory
}

View File

@ -1,62 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func PanelDeleteHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
messageIdStr := ctx.Param("msg")
messageId, err := strconv.ParseUint(messageIdStr, 10, 64); if err != nil {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels", guildId))
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get CSRF token
csrfCorrect := ctx.Query("csrf") == store.Get("csrf").(string)
if !csrfCorrect {
ctx.Redirect(302, "/")
return
}
go table.DeletePanel(messageId)
ctx.Redirect(302, fmt.Sprintf("/manage/%d/panels", guildId))
} else {
ctx.Redirect(302, "/login")
}
}

View File

@ -2,107 +2,18 @@ package manage
import ( import (
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv"
) )
type wrappedPanel struct {
MessageId uint64
ChannelName string
Title string
Content string
CategoryName string
}
func PanelHandler(ctx *gin.Context) { func PanelHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil { guildId := ctx.Keys["guildid"].(uint64)
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get active panels
panelChan := make(chan []table.Panel)
go table.GetPanelsByGuild(guildId, panelChan)
panels := <-panelChan
// Get channels
channels := cache.Instance.GetGuildChannels(guildId)
// Convert to wrapped panels
wrappedPanels := make([]wrappedPanel, 0)
for _, panel := range panels {
wrapper := wrappedPanel{
MessageId: panel.MessageId,
Title: panel.Title,
Content: panel.Content,
CategoryName: "",
}
// Get channel name & category name
for _, guildChannel := range channels {
if guildChannel.Id == panel.ChannelId {
wrapper.ChannelName = guildChannel.Name
} else if guildChannel.Id == panel.TargetCategory {
wrapper.CategoryName = guildChannel.Name
}
}
wrappedPanels = append(wrappedPanels, wrapper)
}
// Get is premium
isPremiumChan := make(chan bool)
go utils.IsPremiumGuild(store, guildId, isPremiumChan)
isPremium := <-isPremiumChan
ctx.HTML(200, "manage/panels", gin.H{ ctx.HTML(200, "manage/panels", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildId,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl, "baseUrl": config.Conf.Server.BaseUrl,
"panelcount": len(panels),
"premium": isPremium,
"panels": wrappedPanels,
"channels": channels,
"validTitle": ctx.Query("validTitle") != "true",
"validContent": ctx.Query("validContent") != "false",
"validColour": ctx.Query("validColour") != "false",
"validChannel": ctx.Query("validChannel") != "false",
"validCategory": ctx.Query("validCategory") != "false",
"validReaction": ctx.Query("validReaction") != "false",
"created": ctx.Query("created") == "true",
"metQuota": ctx.Query("metQuota") == "true",
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -1,70 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"strconv"
)
func SendMessage(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get ticket UUID from URL and verify it exists
ticketChan := make(chan table.Ticket)
go table.GetTicket(ctx.Param("uuid"), ticketChan)
ticket := <-ticketChan
exists := ticket != table.Ticket{}
// Verify that the user has permission to be here
if ticket.Guild != guildId {
ctx.Redirect(302, fmt.Sprintf("/manage/%s/tickets", guildIdStr))
return
}
if exists {
content := fmt.Sprintf("**%s**: %s", store.Get("name").(string), ctx.PostForm("message"))
if len(content) > 2000 {
content = content[0:1999]
}
_, _ = rest.CreateMessage(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Channel, rest.CreateMessageData{Content: content})
}
} else {
ctx.Redirect(302, "/login")
}
ctx.Redirect(301, ctx.Request.URL.String())
}

View File

@ -14,12 +14,7 @@ import (
func SettingsHandler(ctx *gin.Context) { func SettingsHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store) userId := utils.GetUserId(store)
// Verify the guild exists // Verify the guild exists
@ -45,14 +40,6 @@ func SettingsHandler(ctx *gin.Context) {
return return
} }
// Get settings from database
prefix := table.GetPrefix(guildId)
welcomeMessage := table.GetWelcomeMessage(guildId)
limit := table.GetTicketLimit(guildId)
pingEveryone := table.GetPingEveryone(guildId)
archiveChannel := table.GetArchiveChannel(guildId)
categoryId := table.GetChannelCategory(guildId)
namingSchemeChan := make(chan table.NamingScheme) namingSchemeChan := make(chan table.NamingScheme)
go table.GetTicketNamingScheme(guildId, namingSchemeChan) go table.GetTicketNamingScheme(guildId, namingSchemeChan)
namingScheme := <-namingSchemeChan namingScheme := <-namingSchemeChan
@ -83,25 +70,16 @@ func SettingsHandler(ctx *gin.Context) {
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildIdStr,
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"prefix": prefix,
"welcomeMessage": welcomeMessage,
"ticketLimit": limit,
"categories": categories, "categories": categories,
"activecategory": categoryId,
"channels": channels, "channels": channels,
"archivechannel": archiveChannel,
"invalidPrefix": invalidPrefix, "invalidPrefix": invalidPrefix,
"invalidWelcomeMessage": invalidWelcomeMessage, "invalidWelcomeMessage": invalidWelcomeMessage,
"invalidTicketLimit": invalidTicketLimit, "invalidTicketLimit": invalidTicketLimit,
"csrf": store.Get("csrf").(string), "csrf": store.Get("csrf").(string),
"pingEveryone": pingEveryone,
"paneltitle": panelSettings.Title, "paneltitle": panelSettings.Title,
"panelcontent": panelSettings.Content, "panelcontent": panelSettings.Content,
"panelcolour": strconv.FormatInt(int64(panelSettings.Colour), 16), "panelcolour": strconv.FormatInt(int64(panelSettings.Colour), 16),
"usersCanClose": usersCanClose, "usersCanClose": usersCanClose,
"namingScheme": string(namingScheme), "namingScheme": string(namingScheme),
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -1,76 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/messagequeue"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func TicketCloseHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get CSRF token
csrfCorrect := ctx.PostForm("csrf") == store.Get("csrf").(string)
if !csrfCorrect {
ctx.Redirect(302, "/")
return
}
// Get the UUID
uuid := ctx.Param("uuid")
// Verify that tbe ticket exists
ticketChan := make(chan table.Ticket)
go table.GetTicket(uuid, ticketChan)
ticket := <-ticketChan
if ticket.Uuid == "" {
ctx.Redirect(302, fmt.Sprintf("/manage/%d/tickets/view/%s?sucess=false", guildId, uuid))
return
}
// Get the reason
reason := ctx.PostForm("reason")
if len(reason) > 255 {
reason = reason[:255]
}
go messagequeue.Client.PublishTicketClose(ticket.Uuid, userId, reason)
ctx.Redirect(302, fmt.Sprintf("/manage/%d/tickets", guildId))
} else {
ctx.Redirect(302, "/login")
}
}

View File

@ -2,101 +2,18 @@ package manage
import ( import (
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strconv"
"strings"
) )
func TicketListHandler(ctx *gin.Context) { func TicketListHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil { guildId := ctx.Keys["guildid"].(uint64)
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
tickets := table.GetOpenTickets(guildId)
var toFetch []uint64
for _, ticket := range tickets {
toFetch = append(toFetch, ticket.Owner)
for _, idStr := range strings.Split(ticket.Members, ",") {
if memberId, err := strconv.ParseUint(idStr, 10, 64); err == nil {
toFetch = append(toFetch, memberId)
}
}
}
nodes := make(map[uint64]table.UsernameNode)
for _, node := range table.GetUserNodes(toFetch) {
nodes[node.Id] = node
}
var ticketsFormatted []map[string]interface{}
for _, ticket := range tickets {
var membersFormatted []map[string]interface{}
for index, memberIdStr := range strings.Split(ticket.Members, ",") {
if memberId, err := strconv.ParseUint(memberIdStr, 10, 64); err == nil {
if memberId != 0 {
var separator string
if index != len(strings.Split(ticket.Members, ",")) - 1 {
separator = ", "
}
membersFormatted = append(membersFormatted, map[string]interface{}{
"username": nodes[memberId].Name,
"discrim": nodes[memberId].Discriminator,
"sep": separator,
})
}
}
}
ticketsFormatted = append(ticketsFormatted, map[string]interface{}{
"uuid": ticket.Uuid,
"ticketId": ticket.TicketId,
"username": nodes[ticket.Owner].Name,
"discrim": nodes[ticket.Owner].Discriminator,
"members": membersFormatted,
})
}
ctx.HTML(200, "manage/ticketlist", gin.H{ ctx.HTML(200, "manage/ticketlist", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildId,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl, "baseUrl": config.Conf.Server.BaseUrl,
"tickets": ticketsFormatted,
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -1,117 +1,20 @@
package manage package manage
import ( import (
"fmt"
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"regexp"
"strconv"
"strings"
) )
var MentionRegex, _ = regexp.Compile("<@(\\d+)>")
func TicketViewHandler(ctx *gin.Context) { func TicketViewHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil { guildId := ctx.Keys["guildid"].(uint64)
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get ticket UUID from URL and verify it exists
uuid := ctx.Param("uuid")
ticketChan := make(chan table.Ticket)
go table.GetTicket(uuid, ticketChan)
ticket := <-ticketChan
exists := ticket != table.Ticket{}
// If invalid ticket UUID, take user to ticket list
if !exists {
ctx.Redirect(302, fmt.Sprintf("/manage/%s/tickets", guildIdStr))
return
}
// Verify that the user has permission to be here
if ticket.Guild != guildId {
ctx.Redirect(302, fmt.Sprintf("/manage/%s/tickets", guildIdStr))
return
}
// Get messages
messages, err := rest.GetChannelMessages(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Channel, rest.GetChannelMessagesData{Limit: 100})
// Format messages, exclude unneeded data
var messagesFormatted []map[string]interface{}
for _, message := range utils.Reverse(messages) {
content := message.Content
// Format mentions properly
match := MentionRegex.FindAllStringSubmatch(content, -1)
for _, mention := range match {
if len(mention) >= 2 {
mentionedId, err := strconv.ParseUint(mention[1], 10, 64)
if err != nil {
continue
}
ch := make(chan string)
go table.GetUsername(mentionedId, ch)
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", <-ch))
}
}
messagesFormatted = append(messagesFormatted, map[string]interface{}{
"username": message.Author.Username,
"content": content,
})
}
premium := make(chan bool)
go utils.IsPremiumGuild(store, guildId, premium)
ctx.HTML(200, "manage/ticketview", gin.H{ ctx.HTML(200, "manage/ticketview", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"guildId": guildIdStr, "guildId": guildId,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl, "baseUrl": config.Conf.Server.BaseUrl,
"isError": false, "uuid": ctx.Param("uuid"),
"error": "",
"messages": messagesFormatted,
"ticketId": ticket.TicketId,
"uuid": ticket.Uuid,
"include_mock": true,
"premium": <-premium,
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -1,136 +0,0 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/channel"
"strconv"
)
func UpdateSettingsHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store)
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseUint(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildId, false)
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, userId, isAdmin)
if !<-isAdmin {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get CSRF token
csrfCorrect := ctx.PostForm("csrf") == store.Get("csrf").(string)
if !csrfCorrect {
ctx.Redirect(302, "/")
return
}
// Get prefix
prefix := ctx.PostForm("prefix")
prefixValid := false
if prefix != "" && len(prefix) < 8 {
table.UpdatePrefix(guildId, prefix)
prefixValid = true
}
// Get welcome message
welcomeMessageValid := false
welcomeMessage := ctx.PostForm("welcomeMessage")
if welcomeMessage != "" && len(welcomeMessage) < 1000 {
table.UpdateWelcomeMessage(guildId, welcomeMessage)
welcomeMessageValid = true
}
// Get ticket limit
var limit int
limitStr := ctx.PostForm("ticketlimit")
// Verify input is an int and overwrite default limit
if utils.IsInt(limitStr) {
limit, _ = strconv.Atoi(limitStr)
}
// Update limit, or get current limit if user input is invalid
ticketLimitValid := false
if limitStr != "" && utils.IsInt(limitStr) && limit >= 1 && limit <= 10 {
table.UpdateTicketLimit(guildId, limit)
ticketLimitValid = true
}
// Ping everyone
pingEveryone := ctx.PostForm("pingeveryone") == "on"
table.UpdatePingEveryone(guildId, pingEveryone)
// Get a list of actual category IDs
channels := cache.Instance.GetGuildChannels(guildId)
// Update category
if categoryId, err := strconv.ParseUint(ctx.PostForm("category"), 10, 64); err == nil {
for _, ch := range channels {
if ch.Id == categoryId { // compare ID
if ch.Type == channel.ChannelTypeGuildCategory { // verify we're dealing with a category
table.UpdateChannelCategory(guildId, categoryId)
}
break
}
}
}
// Archive channel
if archiveChannelId, err := strconv.ParseUint(ctx.PostForm("archivechannel"), 10, 64); err == nil {
for _, ch := range channels {
if ch.Id == archiveChannelId { // compare ID
if ch.Type == channel.ChannelTypeGuildText { // verify channel type
table.UpdateArchiveChannel(guildId, archiveChannelId)
}
break
}
}
}
// Users can close
usersCanClose := ctx.PostForm("userscanclose") == "on"
table.SetUserCanClose(guildId, usersCanClose)
// Get naming scheme
namingScheme := table.NamingScheme(ctx.PostForm("namingscheme"))
isValidScheme := false
for _, validNamingScheme := range table.Schemes {
if validNamingScheme == namingScheme {
isValidScheme = true
break
}
}
if isValidScheme {
go table.SetTicketNamingScheme(guildId, namingScheme)
}
ctx.Redirect(302, fmt.Sprintf("/manage/%d/settings?validPrefix=%t&validWelcomeMessage=%t&validTicketLimit=%t", guildId, prefixValid, welcomeMessageValid, ticketLimitValid))
} else {
ctx.Redirect(302, "/login")
}
}

View File

@ -1,22 +1,14 @@
package manage package manage
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/rxdn/gdl/rest"
"net/http"
"strconv" "strconv"
"sync" "sync"
"time"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@ -47,12 +39,7 @@ type (
func WebChatWs(ctx *gin.Context) { func WebChatWs(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
@ -105,9 +92,10 @@ func WebChatWs(ctx *gin.Context) {
data := evnt.Data.(map[string]interface{}) data := evnt.Data.(map[string]interface{})
guildId = data["guild"].(string) guildId = data["guild"].(string)
ticket, err = strconv.Atoi(data["ticket"].(string)); ticket, err = strconv.Atoi(data["ticket"].(string))
if err != nil { if err != nil {
conn.Close() conn.Close()
break
} }
socket.Guild = guildId socket.Guild = guildId
@ -135,81 +123,11 @@ func WebChatWs(ctx *gin.Context) {
// Verify the guild is premium // Verify the guild is premium
premium := make(chan bool) premium := make(chan bool)
go utils.IsPremiumGuild(store, guildIdParsed, premium) go utils.IsPremiumGuild(guildIdParsed, premium)
if !<-premium { if !<-premium {
conn.Close() conn.Close()
return return
} }
} else if evnt.Type == "send" {
data := evnt.Data.(string)
if data == "" {
continue
}
// Get ticket UUID from URL and verify it exists
ticketChan := make(chan table.Ticket)
go table.GetTicketById(guildIdParsed, ticket, ticketChan)
ticket := <-ticketChan
exists := ticket != table.Ticket{}
if exists {
content := data
if len(content) > 2000 {
content = content[0:1999]
}
// Preferably send via a webhook
webhookChan := make(chan *string)
go table.GetWebhookByUuid(ticket.Uuid, webhookChan)
webhook := <-webhookChan
success := false
if webhook != nil {
success = executeWebhook( ticket.Uuid, *webhook, content, store)
}
if !success {
content = fmt.Sprintf("**%s**: %s", store.Get("name").(string), data)
if len(content) > 2000 {
content = content[0:1999]
}
_, _ = rest.CreateMessage(config.Conf.Bot.Token, ratelimit.Ratelimiter, ticket.Channel, rest.CreateMessageData{Content: content})
} }
} }
} }
}
}
}
func executeWebhook(uuid, webhook, content string, store sessions.Session) bool {
body := map[string]interface{}{
"content": content,
"username": store.Get("name").(string),
"avatar_url": store.Get("avatar").(string),
}
encoded, err := json.Marshal(&body); if err != nil {
return false
}
req, err := http.NewRequest("POST", fmt.Sprintf("https://canary.discordapp.com/api/webhooks/%s", webhook), bytes.NewBuffer(encoded)); if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
client.Timeout = 3 * time.Second
res, err := client.Do(req); if err != nil {
return false
}
if res.StatusCode == 404 || res.StatusCode == 403 {
go table.DeleteWebhookByUuid(uuid)
} else {
return true
}
return false
}

View File

@ -11,12 +11,6 @@ import (
func IndexHandler(ctx *gin.Context) { func IndexHandler(ctx *gin.Context) {
store := sessions.Default(ctx) store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userId := utils.GetUserId(store) userId := utils.GetUserId(store)
userGuilds := table.GetGuilds(userId) userGuilds := table.GetGuilds(userId)
@ -38,12 +32,6 @@ func IndexHandler(ctx *gin.Context) {
ctx.HTML(200, "main/index", gin.H{ ctx.HTML(200, "main/index", gin.H{
"name": store.Get("name").(string), "name": store.Get("name").(string),
"baseurl": config.Conf.Server.BaseUrl, "baseurl": config.Conf.Server.BaseUrl,
"servers": adminGuilds,
"empty": len(adminGuilds) == 0,
"isIndex": true,
"avatar": store.Get("avatar").(string), "avatar": store.Get("avatar").(string),
}) })
} else {
ctx.Redirect(302, "/login")
}
} }

View File

@ -0,0 +1,24 @@
package middleware
import (
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
)
func AuthenticateCookie(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
ctx.Redirect(302, "/login")
ctx.Abort()
return
}
if !utils.IsLoggedIn(store) {
ctx.Redirect(302, "/login")
ctx.Abort()
return
}
ctx.Keys["userid"] = utils.GetUserId(store)
}

View File

@ -0,0 +1,71 @@
package middleware
import (
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
// requires AuthenticateCookie middleware to be run before
func AuthenticateGuild(isApiMethod bool) gin.HandlerFunc {
return func(ctx *gin.Context) {
if guildId, ok := ctx.Params.Get("id"); ok {
parsed, err := strconv.ParseUint(guildId, 10, 64)
if err != nil {
if isApiMethod {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
ctx.Abort()
} else {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid guild ID",
})
}
return
}
ctx.Keys["guildid"] = parsed
guild, found := cache.Instance.GetGuild(parsed, false)
if !found {
if isApiMethod {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
} else {
ctx.Redirect(302, fmt.Sprintf("https://invite.ticketsbot.net/?guild_id=%d&disable_guild_select=true&response_type=code&scope=bot%%20identify&redirect_uri=%s", parsed, config.Conf.Server.BaseUrl))
}
ctx.Abort()
return
}
ctx.Keys["guild"] = guild
// Verify the user has permissions to be here
isAdmin := make(chan bool)
go utils.IsAdmin(guild, ctx.Keys["userid"].(uint64), isAdmin)
if !<-isAdmin {
if isApiMethod {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
ctx.Abort()
} else {
ctx.AbortWithStatusJSON(403, gin.H{
"success": false,
"error": "Unauthorized",
})
}
}
} else {
if isApiMethod {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
ctx.Abort()
} else {
ctx.AbortWithStatusJSON(400, gin.H{
"success": false,
"error": "Invalid guild ID",
})
}
}
}
}

View File

@ -0,0 +1,54 @@
package middleware
import (
"errors"
"fmt"
"github.com/TicketsBot/GoPanel/config"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"strconv"
)
func AuthenticateToken(ctx *gin.Context) {
header := ctx.GetHeader("Authorization")
token, err := jwt.Parse(header, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.Conf.Server.Secret), nil
})
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{
"error": err.Error(),
})
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
userId, hasUserId := claims["userid"]
if !hasUserId {
ctx.AbortWithStatusJSON(403, gin.H{
"error": errors.New("token is invalid"),
})
return
}
parsedId, err := strconv.ParseUint(userId.(string), 10, 64)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{
"error": errors.New("token is invalid"),
})
return
}
ctx.Keys["userid"] = parsedId
} else {
ctx.AbortWithStatusJSON(403, gin.H{
"error": errors.New("token is invalid"),
})
return
}
}

View File

@ -2,8 +2,10 @@ package http
import ( import (
"fmt" "fmt"
"github.com/TicketsBot/GoPanel/app/http/endpoints/api"
"github.com/TicketsBot/GoPanel/app/http/endpoints/manage" "github.com/TicketsBot/GoPanel/app/http/endpoints/manage"
"github.com/TicketsBot/GoPanel/app/http/endpoints/root" "github.com/TicketsBot/GoPanel/app/http/endpoints/root"
"github.com/TicketsBot/GoPanel/app/http/middleware"
"github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/config"
"github.com/gin-contrib/multitemplate" "github.com/gin-contrib/multitemplate"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
@ -31,33 +33,66 @@ func StartServer() {
// Handle static asset requests // Handle static asset requests
router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false))) router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false)))
router.Use(gin.Recovery())
// Register templates // Register templates
router.HTMLRender = createRenderer() router.HTMLRender = createRenderer()
router.GET("/", root.IndexHandler)
router.GET("/login", root.LoginHandler) router.GET("/login", root.LoginHandler)
router.GET("/callback", root.CallbackHandler) router.GET("/callback", root.CallbackHandler)
router.GET("/logout", root.LogoutHandler)
router.GET("/manage/:id/settings", manage.SettingsHandler) router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect
router.POST("/manage/:id/settings", manage.UpdateSettingsHandler)
router.GET("/manage/:id/logs/page/:page", manage.LogsHandler) authorized := router.Group("/", middleware.AuthenticateCookie)
router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) {
authorized.POST("/token", api.TokenHandler)
router.GET("/manage/:id/blacklist", manage.BlacklistHandler) authenticateGuild := authorized.Group("/", middleware.AuthenticateGuild(false))
router.GET("/manage/:id/blacklist/remove/:user", manage.BlacklistRemoveHandler)
router.GET("/manage/:id/panels", manage.PanelHandler) authorized.GET("/", root.IndexHandler)
router.POST("/manage/:id/panels/create", manage.PanelCreateHandler) authorized.GET("/logout", root.LogoutHandler)
router.GET("/manage/:id/panels/delete/:msg", manage.PanelDeleteHandler)
router.GET("/manage/:id/tickets", manage.TicketListHandler) authenticateGuild.GET("/manage/:id/settings", manage.SettingsHandler)
router.GET("/manage/:id/tickets/view/:uuid", manage.TicketViewHandler) authenticateGuild.GET("/manage/:id/logs", manage.LogsHandler)
router.POST("/manage/:id/tickets/view/:uuid/close", manage.TicketCloseHandler) authenticateGuild.GET("/manage/:id/blacklist", manage.BlacklistHandler)
router.POST("/manage/:id/tickets/view/:uuid", manage.SendMessage) authenticateGuild.GET("/manage/:id/panels", manage.PanelHandler)
router.GET("/webchat", manage.WebChatWs)
authenticateGuild.GET("/manage/:id/tickets", manage.TicketListHandler)
authenticateGuild.GET("/manage/:id/tickets/view/:uuid", manage.TicketViewHandler)
authenticateGuild.POST("/manage/:id/tickets/view/:uuid", api.SendMessage)
authorized.GET("/webchat", manage.WebChatWs)
}
apiGroup := router.Group("/api", middleware.AuthenticateToken)
guildAuthApi := apiGroup.Group("/", middleware.AuthenticateGuild(true))
{
guildAuthApi.GET("/:id/channels", api.ChannelsHandler)
guildAuthApi.GET("/:id/premium", api.PremiumHandler)
guildAuthApi.GET("/:id/settings", api.GetSettingsHandler)
guildAuthApi.POST("/:id/settings", api.UpdateSettingsHandler)
guildAuthApi.GET("/:id/blacklist", api.GetBlacklistHandler)
guildAuthApi.PUT("/:id/blacklist", api.AddBlacklistHandler)
guildAuthApi.DELETE("/:id/blacklist/:user", api.RemoveBlacklistHandler)
guildAuthApi.GET("/:id/panels", api.ListPanels)
guildAuthApi.PUT("/:id/panels", api.CreatePanel)
guildAuthApi.DELETE("/:id/panels/:message", api.DeletePanel)
guildAuthApi.GET("/:id/logs/:page", api.GetLogs)
guildAuthApi.GET("/:id/tickets", api.GetTickets)
guildAuthApi.GET("/:id/tickets/:uuid", api.GetTicket)
guildAuthApi.POST("/:id/tickets/:uuid", api.SendMessage)
guildAuthApi.DELETE("/:id/tickets/:uuid", api.CloseTicket)
}
userGroup := router.Group("/user", middleware.AuthenticateToken)
{
userGroup.GET("/guilds", api.GetGuilds)
}
if err := router.Run(config.Conf.Server.Host); err != nil { if err := router.Run(config.Conf.Server.Host); err != nil {
panic(err) panic(err)

View File

@ -22,6 +22,7 @@ type (
MainSite string MainSite string
Ratelimit Ratelimit Ratelimit Ratelimit
Session Session Session Session
Secret string
} }
Ratelimit struct { Ratelimit struct {

View File

@ -18,9 +18,8 @@ func UpdateArchiveChannel(guildId uint64, channelId uint64) {
database.Database.Where(ArchiveChannel{Guild: guildId}).Assign(ArchiveChannel{Channel: channelId}).FirstOrCreate(&channel) database.Database.Where(ArchiveChannel{Guild: guildId}).Assign(ArchiveChannel{Channel: channelId}).FirstOrCreate(&channel)
} }
func GetArchiveChannel(guildId uint64) uint64 { func GetArchiveChannel(guildId uint64, ch chan uint64) {
var channel ArchiveChannel var channel ArchiveChannel
database.Database.Where(&ArchiveChannel{Guild: guildId}).First(&channel) database.Database.Where(&ArchiveChannel{Guild: guildId}).First(&channel)
ch <- channel.Channel
return channel.Channel
} }

View File

@ -25,9 +25,7 @@ func AddBlacklist(guildId, userId uint64) {
} }
func RemoveBlacklist(guildId, userId uint64) { func RemoveBlacklist(guildId, userId uint64) {
var node BlacklistNode database.Database.Where(BlacklistNode{Guild: guildId, User: userId}).Delete(BlacklistNode{})
database.Database.Where(BlacklistNode{Guild: guildId, User: userId}).Take(&node)
database.Database.Delete(&node)
} }
func GetBlacklistNodes(guildId uint64) []BlacklistNode { func GetBlacklistNodes(guildId uint64) []BlacklistNode {

View File

@ -17,9 +17,8 @@ func UpdateChannelCategory(guildId uint64, categoryId uint64) {
database.Database.Where(&ChannelCategory{GuildId: guildId}).Assign(&ChannelCategory{Category: categoryId}).FirstOrCreate(&ChannelCategory{}) database.Database.Where(&ChannelCategory{GuildId: guildId}).Assign(&ChannelCategory{Category: categoryId}).FirstOrCreate(&ChannelCategory{})
} }
func GetChannelCategory(guildId uint64) uint64 { func GetChannelCategory(guildId uint64, ch chan uint64) {
var category ChannelCategory var category ChannelCategory
database.Database.Where(&ChannelCategory{GuildId: guildId}).First(&category) database.Database.Where(&ChannelCategory{GuildId: guildId}).First(&category)
ch <- category.Category
return category.Category
} }

View File

@ -10,7 +10,7 @@ type Panel struct {
GuildId uint64 `gorm:"column:GUILDID"` // Might be useful in the future so we store it GuildId uint64 `gorm:"column:GUILDID"` // Might be useful in the future so we store it
Title string `gorm:"column:TITLE;type:VARCHAR(255)"` Title string `gorm:"column:TITLE;type:VARCHAR(255)"`
Content string `gorm:"column:CONTENT;type:TEXT"` Content string `gorm:"column:CONTENT;type:TEXT"`
Colour int `gorm:"column:COLOUR` Colour uint32 `gorm:"column:COLOUR`
TargetCategory uint64 `gorm:"column:TARGETCATEGORY"` TargetCategory uint64 `gorm:"column:TARGETCATEGORY"`
ReactionEmote string `gorm:"column:REACTIONEMOTE;type:VARCHAR(32)"` ReactionEmote string `gorm:"column:REACTIONEMOTE;type:VARCHAR(32)"`
} }
@ -19,7 +19,7 @@ func (Panel) TableName() string {
return "panels" return "panels"
} }
func AddPanel(messageId, channelId, guildId uint64, title, content string, colour int, targetCategory uint64, reactionEmote string) { func AddPanel(messageId, channelId, guildId uint64, title, content string, colour uint32, targetCategory uint64, reactionEmote string) {
database.Database.Create(&Panel{ database.Database.Create(&Panel{
MessageId: messageId, MessageId: messageId,
ChannelId: channelId, ChannelId: channelId,
@ -45,6 +45,12 @@ func GetPanelsByGuild(guildId uint64, ch chan []Panel) {
ch <- panels ch <- panels
} }
func GetPanel(messageId uint64, ch chan Panel) {
var row Panel
database.Database.Where(Panel{MessageId: messageId}).Take(&row)
ch <- row
}
func DeletePanel(msgId uint64) { func DeletePanel(msgId uint64) {
database.Database.Where(Panel{MessageId: msgId}).Delete(Panel{}) database.Database.Where(Panel{MessageId: msgId}).Delete(Panel{})
} }

View File

@ -29,9 +29,8 @@ func UpdatePingEveryone(guildId uint64, pingEveryone bool) {
//database.Database.Where(&PingEveryone{GuildId: guildId}).Assign(&updated).FirstOrCreate(&PingEveryone{}) //database.Database.Where(&PingEveryone{GuildId: guildId}).Assign(&updated).FirstOrCreate(&PingEveryone{})
} }
func GetPingEveryone(guildId uint64) bool { func GetPingEveryone(guildId uint64, ch chan bool) {
pingEveryone := PingEveryone{PingEveryone: true} pingEveryone := PingEveryone{PingEveryone: true}
database.Database.Where(&PingEveryone{GuildId: guildId}).First(&pingEveryone) database.Database.Where(&PingEveryone{GuildId: guildId}).First(&pingEveryone)
ch <- pingEveryone.PingEveryone
return pingEveryone.PingEveryone
} }

View File

@ -17,9 +17,8 @@ func UpdatePrefix(guildId uint64, prefix string) {
database.Database.Where(&Prefix{GuildId: guildId}).Assign(&Prefix{Prefix: prefix}).FirstOrCreate(&Prefix{}) database.Database.Where(&Prefix{GuildId: guildId}).Assign(&Prefix{Prefix: prefix}).FirstOrCreate(&Prefix{})
} }
func GetPrefix(guildId uint64) string { func GetPrefix(guildId uint64, ch chan string) {
prefix := Prefix{Prefix: "t!"} prefix := Prefix{Prefix: "t!"}
database.Database.Where(&Prefix{GuildId: guildId}).First(&prefix) database.Database.Where(&Prefix{GuildId: guildId}).First(&prefix)
ch <- prefix.Prefix
return prefix.Prefix
} }

View File

@ -17,9 +17,8 @@ func UpdateTicketLimit(guildId uint64, limit int) {
database.Database.Where(&TicketLimit{GuildId: guildId}).Assign(&TicketLimit{Limit: limit}).FirstOrCreate(&TicketLimit{}) database.Database.Where(&TicketLimit{GuildId: guildId}).Assign(&TicketLimit{Limit: limit}).FirstOrCreate(&TicketLimit{})
} }
func GetTicketLimit(guildId uint64) int { func GetTicketLimit(guildId uint64, ch chan int) {
limit := TicketLimit{Limit: 5} limit := TicketLimit{Limit: 5}
database.Database.Where(&TicketLimit{GuildId: guildId}).First(&limit) database.Database.Where(&TicketLimit{GuildId: guildId}).First(&limit)
ch <- limit.Limit
return limit.Limit
} }

View File

@ -17,9 +17,8 @@ func UpdateWelcomeMessage(guildId uint64, message string) {
database.Database.Where(&WelcomeMessage{GuildId: guildId}).Assign(&WelcomeMessage{Message: message}).FirstOrCreate(&WelcomeMessage{}) database.Database.Where(&WelcomeMessage{GuildId: guildId}).Assign(&WelcomeMessage{Message: message}).FirstOrCreate(&WelcomeMessage{})
} }
func GetWelcomeMessage(guildId uint64) string { func GetWelcomeMessage(guildId uint64, ch chan string) {
message := WelcomeMessage{Message: "No message specified"} message := WelcomeMessage{Message: "No message specified"}
database.Database.Where(&WelcomeMessage{GuildId: guildId}).First(&message) database.Database.Where(&WelcomeMessage{GuildId: guildId}).First(&message)
ch <- message.Message
return message.Message
} }

3
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/TicketsBot/archiverclient v0.0.0-20200420161043-3532ff9ea943 github.com/TicketsBot/archiverclient v0.0.0-20200420161043-3532ff9ea943
github.com/apex/log v1.1.2 github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/multitemplate v0.0.0-20200226145339-3e397ee01bc6 github.com/gin-contrib/multitemplate v0.0.0-20200226145339-3e397ee01bc6
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607
@ -21,5 +22,5 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62
github.com/rxdn/gdl v0.0.0-20200417164852-76b2d3c847c1 github.com/rxdn/gdl v0.0.0-20200417164852-76b2d3c847c1
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
) )

View File

@ -26,7 +26,7 @@
align-items: center; align-items: center;
} }
.channel-name { #channel-name {
color: white; color: white;
padding-left: 20px; padding-left: 20px;
} }
@ -56,12 +56,6 @@
min-height: 100%; min-height: 100%;
} }
.form-control:focus {
border-color: #2e3136 !important;
background-color: #2e3136 !important;
color: white !important;
}
.message-input:focus { .message-input:focus {
border-color: #2e3136 !important; border-color: #2e3136 !important;
background-color: #2e3136 !important; background-color: #2e3136 !important;

View File

@ -3,10 +3,19 @@ body {
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
background-color: #121212 !important;
color: white;
}
.card {
background-color: #272727 !important;
}
.card-title {
color: white;
} }
.sidebar { .sidebar {
background: url("/assets/img/sidebar-2.jpg");
background-size: cover; background-size: cover;
overflow-x: hidden !important; overflow-x: hidden !important;
} }
@ -37,3 +46,68 @@ body {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
.form-control, .input-group-text, .form-check-input {
border-color: #2e3136 !important;
background-color: #2e3136 !important;
color: white !important;
}
.server {
color: white;
}
#bg-dark {
background-color: #272727 !important;
}
#sidebar-gradient:after {
background: #272727 !important;
}
.simple-text {
padding: 0 !important;
}
.white {
color: white !important;
}
.icon {
color: white;
float: left;
vertical-align: middle;
padding-right: 5px;
}
.avatar {
width: 32px;
height: 32px;
display: block;
background-size: cover;
border-radius: 50%;
margin-left: 5px;
float: right;
vertical-align: middle;
bottom: 100%;
top: 0;
}
.toast-header {
background-color: #272727 !important;
color: white !important;
}
.toast-body {
background-color: #2e3136 !important;
color: white !important;
}
.toast {
border-radius: .25rem !important;
border-color: #272727 !important;
}
#premium-ad {
display: none;
}

View File

@ -14,16 +14,55 @@
<link href="/assets/css/style.css" rel="stylesheet"> <link href="/assets/css/style.css" rel="stylesheet">
<style> <style>
.avatar { #avatar-sidebar {
background: url("{{.avatar}}?size=32"); background: url("{{.avatar}}?size=256") center center;
width: 28px; width: 32px;
height: 29px; height: 32px;
display: block; display: block;
background-size: cover; background-size: cover;
border-radius: 50%; border-radius: 50%;
float: right;
} }
</style> </style>
<!-- Vue
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"
integrity="sha256-T/f7Sju1ZfNNfBh7skWn0idlCBcI3RwdLSS4/I7NQKQ=" crossorigin="anonymous"></script>
<script>
async function getToken() {
let token = window.localStorage.getItem('token');
if (token == null) {
let res = await axios.post('/token', {
withCredentials: true
});
if (res.status !== 200 || !res.data.success) {
console.log("An error occurred whilst retrieving an authentication token. Please contact the developer");
console.log(res);
return;
}
token = res.data.token;
localStorage.setItem('token', token);
}
return token;
}
async function setDefaultHeader() {
const token = await getToken();
axios.defaults.headers.common['Authorization'] = token;
axios.defaults.validateStatus = false;
}
setDefaultHeader();
</script>
<!-- Bootstrap --> <!-- Bootstrap -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
@ -51,6 +90,49 @@
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-161945537-1', 'auto'); ga('create', 'UA-161945537-1', 'auto');
ga('send', 'pageview'); ga('send', 'pageview');
</script>
<script async src='https://www.google-analytics.com/analytics.js'
type="80be96f83bbfbba3d4097e23-text/javascript"></script>
<script>
function showToast(title, content) {
const container = document.getElementById('toast-container');
container.innerHTML += `
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">${title}</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
${content}
</div>
</div>
`;
$('.toast').toast('show');
}
function appendTd(tr, content) {
const td = document.createElement('td');
td.appendChild(document.createTextNode(content));
td.classList.add('white');
tr.appendChild(td);
}
function appendButton(tr, content, onclick) {
const tdRemove = document.createElement('td');
const removeButton = document.createElement('button');
removeButton.type = 'submit';
removeButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto');
removeButton.appendChild(document.createTextNode(content));
removeButton.onclick = onclick;
tdRemove.appendChild(removeButton);
tr.appendChild(tdRemove);
}
</script> </script>
<script async src='https://www.google-analytics.com/analytics.js' type="80be96f83bbfbba3d4097e23-text/javascript"></script>
{{end}} {{end}}

View File

@ -1,23 +1,32 @@
{{define "navbar"}} {{define "navbar"}}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark" id="bg-dark">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/settings">Settings</a> <a class="nav-link" href="/manage/{{.guildId}}/settings"><i class="fas fa-cogs icon"></i>Settings</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/logs/page/1">Logs</a> <a class="nav-link" href="/manage/{{.guildId}}/logs"><i class="fas fa-copy icon"></i>Logs</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/blacklist">Blacklist</a> <a class="nav-link" href="/manage/{{.guildId}}/blacklist"><i class="fas fa-ban icon"></i>Blacklist</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/tickets">Ticket List</a> <a class="nav-link" href="/manage/{{.guildId}}/tickets"><i class="fas fa-ticket-alt icon"></i>Ticket List</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/panels">Reaction Panels</a> <a class="nav-link" href="/manage/{{.guildId}}/panels"><i class="fas fa-mouse-pointer icon"></i>Reaction Panels</a>
</li>
</ul>
<ul class="navbar-nav navbar-right">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-server icon"></i>Servers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout"><i class="fas fa-sign-out-alt icon"></i>Logout</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">{{.name}}<img class="avatar" src="{{.avatar}}?size=256" alt="Avatar"/></a>
</li> </li>
</ul> </ul>
</div>
</nav> </nav>
{{end}} {{end}}

View File

@ -1,12 +1,6 @@
{{define "sidebar"}} {{define "sidebar"}}
<div class="sidebar" data-color="orange" data-image="/assets/img/sidebar-2.jpg"> <div class="sidebar" id="sidebar-gradient">
<div class="sidebar-wrapper"> <div class="sidebar-wrapper">
<div class="logo">
<a href="https://ticketsbot.net" class="simple-text">
TicketsBot
</a>
</div>
<ul class="nav"> <ul class="nav">
<li class="nav-item sidebar-bottom"> <li class="nav-item sidebar-bottom">
<a href="/"> <a href="/">
@ -23,7 +17,7 @@
</li> </li>
<li class="nav-item sidebar-bottom" style="bottom: 10px"> <li class="nav-item sidebar-bottom" style="bottom: 10px">
<a href="#"> <a href="#">
<i class="avatar"></i> <i id="avatar-sidebar"></i>
<p>{{.name}}</p> <p>{{.name}}</p>
</a> </a>
</li> </li>

View File

@ -1,13 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="">
<head> <head>
{{template "head" .}} {{template "head" .}}
</head> </head>
<body> <body>
<div class="wrapper"> <div class="wrapper">
{{template "sidebar" .}} <div class="main-panel" style="width: 100% !important;">
<div class="main-panel">
{{template "navbar" .}} {{template "navbar" .}}
{{template "content" .}} {{template "content" .}}
</div> </div>

View File

@ -12,27 +12,27 @@
<div id="accordion"> <div id="accordion">
<div class="card"> <div class="card">
<div class="card-header collapsed filterCard" id="addBlacklistHeader" data-toggle="collapse" data-target="#addBlacklist" aria-expanded="false" aria-controls="addBlacklist"> <div class="card-header collapsed filterCard" id="addBlacklistHeader" data-toggle="collapse" data-target="#addBlacklist" aria-expanded="false" aria-controls="addBlacklist">
<span class="align-middle" data-toggle="collapse" data-target="#addBlacklist" aria-expanded="false" aria-controls="addBlacklist"> <span class="align-middle white" data-toggle="collapse" data-target="#addBlacklist" aria-expanded="false" aria-controls="addBlacklist">
<i class="fas fa-plus"></i> Add New User <i class="fas fa-plus"></i> Add New User
</span> </span>
</div> </div>
<div id="addBlacklist" class="collapse" aria-labelledby="addBlacklistHeader" data-parent="#accordion"> <div id="addBlacklist" class="collapse" aria-labelledby="addBlacklistHeader" data-parent="#accordion">
<div class="card-body"> <div class="card-body">
<form> <form onsubmit="blacklistUser(); return false;">
<div class="row"> <div class="row">
<div class="col-md-3 pr-1"> <div class="col-md-3 pr-1">
<div class="form-group"> <div class="form-group">
<label>Username</label> <label>Username</label>
<input name="username" type="text" class="form-control" placeholder="Username"> <input name="username" type="text" class="form-control" placeholder="Username" id="username">
</div> </div>
</div> </div>
<div class="col-md-1 px-1"> <div class="col-md-2 px-1">
<label>Discriminator</label> <label>Discriminator</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text">#</div> <div class="input-group-text">#</div>
</div> </div>
<input name="discrim" type="text" class="form-control" placeholder="0000"> <input name="discrim" type="text" class="form-control" placeholder="0000" id="discriminator">
</div> </div>
</div> </div>
</div> </div>
@ -41,7 +41,7 @@
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary mx-auto"><i class="fas fa-paper-plane"></i> Submit</button> <button type="submit" class="btn btn-primary btn-fill mx-auto"><i class="fas fa-paper-plane"></i> Submit</button>
</div> </div>
</div> </div>
</div> </div>
@ -59,14 +59,7 @@
<th>Remove</th> <th>Remove</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="blacklisted-container">
{{range .blacklisted}}
<tr>
<td>{{.userId}}</td>
<td>{{.username}}#{{.discrim}}</td>
<td><a href="/manage/{{$.guildId}}/blacklist/remove/{{.userId}}?c={{$.csrf}}">Remove</a></td>
</tr>
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -76,40 +69,73 @@
</div> </div>
</div> </div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;"> <div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;">
<div style="position: absolute; right: 10px; min-width: 300px"> <div style="position: absolute; right: 10px" id="toast-container">
{{if .userNotFound}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
The user you specified couldn't be found
</div>
</div>
{{end}}
{{if .isStaff}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
You cannot blacklist a staff member
</div>
</div>
{{end}}
</div> </div>
</div> </div>
<script> <script>
$('.toast').toast('show'); async function appendUser(id, username, discrim) {
const container = document.getElementById('blacklisted-container');
const tr = document.createElement('tr');
tr.id = id;
appendTd(tr, id);
appendTd(tr, `${username}#${discrim}`);
const tdRemove = document.createElement('td');
const removeButton = document.createElement('button');
removeButton.type = 'submit';
removeButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto');
removeButton.appendChild(document.createTextNode('Remove'));
removeButton.onclick = () => {unblacklistUser(id)};
tdRemove.appendChild(removeButton);
tr.appendChild(tdRemove);
container.appendChild(tr);
}
async function unblacklistUser(id) {
const res = await axios.delete('/api/{{.guildId}}/blacklist/' + id);
if (res.status === 200 && res.data.success) {
showToast("Success", "Unblacklisted user");
const el = document.getElementById(id);
el.parentNode.removeChild(el);
} else {
showToast("Error", res.data.error)
}
}
async function blacklistUser() {
const username = document.getElementById('username').value;
const discriminator = document.getElementById('discriminator').value;
const res = await axios.put('/api/{{.guildId}}/blacklist', {
'username': username,
'discriminator': discriminator,
});
if (res.status === 200 && res.data.success) {
showToast("Success", "User has been blacklisted");
appendUser(res.data.user_id, username, discriminator);
document.getElementById('username').value = '';
document.getElementById('discriminator').value = '';
} else {
showToast("Error", res.data.error)
}
}
async function loadData() {
const res = await axios.get('/api/{{.guildId}}/blacklist');
Object.keys(res.data).forEach((userId) => {
const userData = res.data[userId];
appendUser(userId, userData.username, userData.discriminator);
});
}
loadData();
</script> </script>
</div> </div>
{{end}} {{end}}

View File

@ -6,41 +6,56 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h4 class="card-title">Servers</h4> <h4 class="card-title">Servers</h4>
{{if .empty}}
<p class="card-category">Select a server to manage below</p>
{{end}}
</div> </div>
<div class="card-body"> <div class="card-body" id="card">
{{if .empty}} <div class="card-body table-responsive">
<p class="center-align" style="padding-top: 50px; font-size: 16px"> <p class="center-align white" style="padding-top: 50px; font-size: 16px; display: none" id="no-guilds">
You are not the admin of any guilds that the bot is in. Click below to invite the bot: You are not the admin of any guilds that the bot is in. Click below to invite the bot:
<br/> <br/>
<a href="https://invite.ticketsbot.net">Invite</a> <a href="https://invite.ticketsbot.net"><button class="btn btn-primary btn-fill"><i class="fas fa-plus"></i> Invite</button></a>
</p> </p>
{{else}} <table class="table table-hover table-striped" id="guild-table" style="display: none">
<div class="card-body table-responsive">
<table class="table table-hover table-striped">
<thead> <thead>
<th>Server Name</th> <th>Server Name</th>
</thead> </thead>
<tbody> <tbody id="guild-container">
{{range .servers}}
<tr>
<td>
<a href="/manage/{{.Id}}/settings">
{{.Name}}
</a>
</td>
</tr>
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
{{end}} </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<script>
async function loadData() {
const res = await axios.get('/user/guilds');
if (res.data.length > 0) {
document.getElementById('guild-table').style.display = 'table';
const container = document.getElementById('guild-container');
for (guild of res.data) {
const tr = document.createElement('tr');
const td = document.createElement('td');
const link = document.createElement('a');
link.href = `/manage/${guild.id}/settings`;
link.classList.add('server');
link.appendChild(document.createTextNode(guild.name));
td.appendChild(link);
tr.appendChild(td);
container.appendChild(tr);
}
} else {
document.getElementById('no-guilds').style.display = 'block';
}
}
loadData();
</script>
{{end}} {{end}}

View File

@ -6,37 +6,37 @@
<div id="accordion"> <div id="accordion">
<div class="card"> <div class="card">
<div class="card-header collapsed filterCard" id="filterHeader" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs"> <div class="card-header collapsed filterCard" id="filterHeader" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs">
<span class="align-middle" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs"> <span class="align-middle white" data-toggle="collapse" data-target="#filterLogs" aria-expanded="false" aria-controls="filterLogs">
<i class="fas fa-search"></i> Filter Logs <i class="fas fa-search"></i> Filter Logs
</span> </span>
</div> </div>
<div id="filterLogs" class="collapse" aria-labelledby="filterHeader" data-parent="#accordion"> <div id="filterLogs" class="collapse" aria-labelledby="filterHeader" data-parent="#accordion">
<div class="card-body"> <div class="card-body">
<form action="/manage/{{.guildId}}/logs/page/1"> <form onsubmit="filterLogs(); return false;">
<div class="row"> <div class="row">
<div class="col-md-4 pr-1"> <div class="col-md-4 pr-1">
<div class="form-group"> <div class="form-group">
<label>Ticket ID</label> <label>Ticket ID</label>
<input name="ticketid" type="text" class="form-control" placeholder="Ticket ID"> <input name="ticketid" type="text" class="form-control" placeholder="Ticket ID" id="ticketid">
</div> </div>
</div> </div>
<div class="col-md-4 px-1"> <div class="col-md-4 px-1">
<div class="form-group"> <div class="form-group">
<label>Username</label> <label>Username</label>
<input name="username" type="text" class="form-control" placeholder="Username"> <input name="username" type="text" class="form-control" placeholder="Username" id="username">
</div> </div>
</div> </div>
<div class="col-md-4 px-1"> <div class="col-md-4 px-1">
<div class="form-group"> <div class="form-group">
<label>User ID</label> <label>User ID</label>
<input name="userid" type="text" class="form-control" placeholder="User ID"> <input name="userid" type="text" class="form-control" placeholder="User ID" id="userid">
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary mx-auto"><i class="fas fa-paper-plane"></i> Filter</button> <button type="submit" class="btn btn-primary mx-auto btn-fill"><i class="fas fa-paper-plane"></i> Filter</button>
</div> </div>
</div> </div>
</div> </div>
@ -64,7 +64,7 @@
<th>Log URL</th> <th>Log URL</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="log-container">
{{range .logs}} {{range .logs}}
<tr> <tr>
<td>{{.ticketid}}</td> <td>{{.ticketid}}</td>
@ -79,14 +79,9 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{{if .isPageOne}} <li class="waves-effect"><a href="#" onclick="previous()"><i class="fas fa-chevron-left"></i></a></li>
<li class="disabled"><a href="#"><i class="fas fa-chevron-left"></i></a></li> <p class="center-align white" style="padding-left: 10px; padding-right: 10px;">Page {{.page}}</p>
{{else}} <li class="waves-effect"><a href="#" onclick="next()"><i class="fas fa-chevron-right"></i></a></li>
<li class="waves-effect"><a href="/manage/{{.guildId}}/logs/page/{{.previousPage}}"><i class="fas fa-chevron-left"></i></a></li>
{{end}}
<p class="center-align" style="padding-left: 10px; padding-right: 10px;">Page {{.page}}</p>
<li class="waves-effect"><a href="/manage/{{.guildId}}/logs/page/{{.nextPage}}"><i class="fas fa-chevron-right"></i></a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -96,5 +91,89 @@
</div> </div>
</div> </div>
</div> </div>
<!--<div aria-live="polite" aria-atomic="true" style="position: relative">
<div style="position: absolute; right: 10px" id="toast-container">
</div>
</div>-->
<script>
let currentPage = 1;
function appendLog(log) {
const container = document.getElementById('log-container');
const tr = document.createElement('tr');
appendTd(tr, log.ticketid);
appendTd(tr, log.username);
appendTd(tr, log.userid);
appendButton(tr, 'View', () => { location.href = '/manage/{{.guildId}}/logs/view/' + log.ticketid });
// create view button
const viewTd = document.createElement('td');
tr.appendChild(viewTd);
container.appendChild(tr);
}
async function loadPage(page, ticketId, username, userId) {
const container = document.getElementById('log-container');
container.innerHTML = '';
let url = '/api/{{.guildId}}/logs/' + page;
if (ticketId !== undefined) {
url += `?ticketid=${ticketId}`;
} else if (username !== undefined) {
url += `?username=${username}`;
} else if (userId !== undefined) {
url += `?userid=${userId}`;
}
const res = await axios.get(url);
for (log of res.data) {
appendLog(log);
}
}
async function loadData() {
await loadPage(currentPage);
}
loadData();
</script>
<script>
async function next() {
currentPage += 1;
await loadPage(currentPage);
}
async function previous() {
if (currentPage <= 1) {
return
}
currentPage -= 1;
await loadPage(currentPage);
}
async function filterLogs() {
const ticketId = document.getElementById('ticketid').value;
const username = document.getElementById('username').value;
const userId = document.getElementById('userid').value;
if (ticketId > 0) {
await loadPage(1, ticketId);
} else if (username !== "") {
await loadPage(1, undefined, username);
} else if (userId !== "") {
await loadPage(1, undefined, undefined, userId);
} else {
await loadPage(1);
}
}
</script>
</div> </div>
{{end}} {{end}}

View File

@ -8,11 +8,8 @@
<h4 class="card-title">Reaction Panels</h4> <h4 class="card-title">Reaction Panels</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<p>Your panel quota: <b>{{.panelcount}} / {{if .premium}}∞{{else}}1{{end}}</b></p> <p class="white">Your panel quota: <b><span id="panel-count"></span> / <span id="panel-quota"></span></b></p>
<p class="white" id="premium-ad">Note: You can create unlimited panels with premium</p>
{{if not .premium}}
<p>Note: You can expand your panel quote by purchasing premium</p>
{{end}}
<table class="table table-hover table-striped"> <table class="table table-hover table-striped">
<thead> <thead>
@ -24,16 +21,7 @@
<th>Delete</th> <th>Delete</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="panel-container">
{{range .panels}}
<tr>
<td>#{{.ChannelName}}</td>
<td>{{.Title}}</td>
<td>{{.Content}}</td>
<td>{{.CategoryName}}</td>
<td><a href="/manage/{{$.guildId}}/panels/delete/{{.MessageId}}?csrf={{$.csrf}}">Delete</a></td>
</tr>
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -43,19 +31,19 @@
<h4 class="card-title">Create A Panel</h4> <h4 class="card-title">Create A Panel</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" action="/manage/{{.guildId}}/panels/create"> <form onsubmit="createPanel(); return false;">
<div class="row"> <div class="row">
<div class="col-md-4 pr-1"> <div class="col-md-4 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black">Panel Title</label> <label class="black">Panel Title</label>
<input name="title" type="text" class="form-control" placeholder="Open a ticket!"> <input name="title" type="text" class="form-control" placeholder="Open a ticket!" id="title">
</div> </div>
</div> </div>
<div class="col-md-8 pr-1"> <div class="col-md-8 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black">Panel Content</label> <label class="black">Panel Content</label>
<textarea name="content" type="text" class="form-control" <textarea name="content" type="text" class="form-control"
placeholder="By reacting to this ticket, a ticket will be opened for you."></textarea> placeholder="By reacting to this ticket, a ticket will be opened for you." id="content"></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -63,7 +51,7 @@
<div class="col-md-3 pr-1"> <div class="col-md-3 pr-1">
<label class="black">Panel Colour</label> <label class="black">Panel Colour</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input name="colour" type="color" class="form-control input-fill" value="#23A31A"> <input name="colour" type="color" class="form-control input-fill" value="#2ECC71" id="colour">
</div> </div>
</div> </div>
@ -73,12 +61,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text">#</div> <div class="input-group-text">#</div>
</div> </div>
<select class="form-control" name="channel"> <select class="form-control" name="channel" id="channel-container">
{{range .channels}}
{{if eq .Type 0}} <!-- Check the channel is a text channel -->
<option value="{{.Id}}">{{.Name}}</option>
{{end}}
{{end}}
</select> </select>
</div> </div>
</div> </div>
@ -89,12 +72,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text">#</div> <div class="input-group-text">#</div>
</div> </div>
<select class="form-control" name="categories"> <select class="form-control" name="categories" id="category-container">
{{range .channels}}
{{if eq .Type 4}} <!-- Check the channel is a category -->
<option value="{{.Id}}">{{.Name}}</option>
{{end}}
{{end}}
</select> </select>
</div> </div>
</div> </div>
@ -102,16 +80,15 @@
<div class="col-md-3 pr-1"> <div class="col-md-3 pr-1">
<div class="form-group"> <div class="form-group">
<label class="black">Reaction Emote</label> <label class="black">Reaction Emote</label>
<input name="reaction" type="text" class="form-control" placeholder="envelope_with_arrow"> <input name="reaction" type="text" class="form-control" placeholder="envelope_with_arrow" id="reaction">
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<input name="csrf" type="hidden" value="{{.csrf}}">
<div class="col-md-2 pr-1 offset-md-5"> <div class="col-md-2 pr-1 offset-md-5">
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Submit</button> <button type="submit" class="btn btn-primary btn-fill"><i class="fas fa-paper-plane"></i> Submit</button>
</div> </div>
</div> </div>
</div> </div>
@ -123,119 +100,148 @@
</div> </div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;"> <div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;">
<div style="position: absolute; right: 10px; min-width: 300px"> <div style="position: absolute; right: 10px" id="toast-container">
{{if not .validTitle}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Panel titles must be between 1 and 255 characters long
</div>
</div>
{{end}}
{{if not .validContent}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Panel content must be between 1 and 1024 characters long
</div>
</div>
{{end}}
{{if not .validColour}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Invalid panel colour. You must use the hex value of the colour, which you can find <a
href="https://www.google.co.uk/search?client=opera&q=html+colour+picker">here</a>.
<br/>Colour has defaulted to green.
</div>
</div>
{{end}}
{{if not .validChannel}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Invalid channel - please try again
</div>
</div>
{{end}}
{{if not .validCategory}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Invalid category - please try again
</div>
</div>
{{end}}
{{if not .validReaction}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Invalid reaction emote
</div>
</div>
{{end}}
{{if .created}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Success</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
Your panel has been created. You may need to refresh this page to see it displayed.
</div>
</div>
{{end}}
{{if .metQuota}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
You've hit your panel quota. Premium users can create <b>unlimited panels</b>. Click <a
href="https://ticketsbot.net/premium">here</a> to learn more about premium.
</div>
</div>
{{end}}
</div> </div>
</div> </div>
<script> <script>
$('.toast').toast('show'); async function getChannels() {
const res = await axios.get('/api/{{.guildId}}/channels');
return res.data;
}
function getChannelName(channels, channelId) {
return channels.find(ch => ch.id === channelId);
}
async function deletePanel(messageId) {
const res = await axios.delete('/api/{{.guildId}}/panels/' + messageId);
if (res.status === 200 && res.data.success) {
showToast('Success', 'Panel deleted successfully');
const el = document.getElementById(messageId);
el.parentNode.removeChild(el);
} else {
showToast('Error', res.data.error);
}
}
async function createPanel() {
const data = {
title: document.getElementById('title').value,
content: document.getElementById('content').value,
emote: document.getElementById('reaction').value.replace(':', ''),
colour: parseInt(`0x${document.getElementById('colour').value.slice(1)}`),
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,
};
// fill defaults
if (data.title === '') {
data.title = 'Open a ticket!';
}
if (data.content === '') {
data.content = 'By reacting to this ticket, a ticket will be opened for you.';
}
if (data.emote === '') {
data.emote = 'envelope_with_arrow';
}
const res = await axios.put('/api/{{.guildId}}/panels', data);
if (res.status === 200 && res.data.success) {
appendPanel(data, await getChannels());
showToast('Success', 'Panel created successfully')
} else {
showToast('Error', res.data.error);
}
}
async function fillChannels(channels) {
const container = document.getElementById('channel-container');
channels.filter(ch => ch.type === 0).forEach(ch => {
const el = document.createElement('option');
el.value = ch.id;
el.appendChild(document.createTextNode(ch.name));
container.appendChild(el);
});
}
async function fillCategories(channels) {
const container = document.getElementById('category-container');
channels.filter(ch => ch.type === 4).forEach(ch => {
const el = document.createElement('option');
el.value = ch.id;
el.appendChild(document.createTextNode(ch.name));
container.appendChild(el);
});
}
// TODO: Update on append / delete
async function fillPanelQuota(panelCount) {
const res = await axios.get('/api/{{.guildId}}/premium');
const el = document.getElementById('panel-quota');
if (res.data.premium) {
el.appendChild(document.createTextNode('∞'));
} else {
el.appendChild(document.createTextNode('1'));
document.getElementById('premium-ad').style.display = 'block';
}
document.getElementById('panel-count').appendChild(document.createTextNode(panelCount));
}
function appendPanel(panel, channels) {
const container = document.getElementById('panel-container');
const tr = document.createElement('tr');
tr.id = panel.message_id; // TODO: When we call this after creating a panel, we don't know the message ID yet
appendTd(tr, `#${getChannelName(channels, panel.channel_id).name}`);
appendTd(tr, panel.title);
appendTd(tr, panel.content);
appendTd(tr, getChannelName(channels, panel.category_id).name);
// build remove button
const deleteTd = document.createElement('td');
const deleteButton = document.createElement('button');
deleteButton.type = 'submit';
deleteButton.classList.add('btn', 'btn-primary', 'btn-fill', 'mx-auto');
deleteButton.appendChild(document.createTextNode('Delete'));
deleteButton.onclick = () => {deletePanel(panel.message_id)};
deleteTd.appendChild(deleteButton);
tr.appendChild(deleteTd);
container.appendChild(tr);
}
async function fillPanels(channels) {
const res = await axios.get('/api/{{.guildId}}/panels');
if (res.status !== 200) {
showToast("Error", res.data);
return 0;
}
for (const panel of res.data) {
appendPanel(panel, channels);
}
return res.data.length;
}
async function loadData() {
const channels = await getChannels();
const panelCount = await fillPanels(channels);
await fillPanelQuota(panelCount);
await fillChannels(channels);
await fillCategories(channels);
}
loadData();
</script> </script>
</div> </div>
{{end}} {{end}}

View File

@ -11,25 +11,25 @@
{{end}} {{end}}
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form onsubmit="updateSettings(); return false;">
<div class="row"> <div class="row">
<div class="col-md-5 pr-1"> <div class="col-md-5 pr-1">
<div class="form-group"> <div class="form-group">
<label>Prefix (Max len. 8)</label> <label>Prefix (Max len. 8)</label>
<input name="prefix" type="text" class="form-control" placeholder="t!" value="{{.prefix}}"> <input name="prefix" type="text" class="form-control" placeholder="t!" id="prefix">
</div> </div>
</div> </div>
<div class="col-md-5 px-1"> <div class="col-md-5 px-1">
<div class="form-group"> <div class="form-group">
<label>Ticket Limit (1-10)</label> <label>Ticket Limit (1-10)</label>
<input name="ticketlimit" type="text" class="form-control" placeholder="5" value="{{.ticketLimit}}"> <input name="ticketlimit" type="text" class="form-control" placeholder="5" id="ticket_limit">
</div> </div>
</div> </div>
<div class="col-md-2 px-1"> <div class="col-md-2 px-1">
<div class="form-group"> <div class="form-group">
<label>Ping @everyone on ticket open</label> <label>Ping @everyone on ticket open</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="pingeveryone" value="on" {{if .pingEveryone}}checked{{end}} style="width:30px;height:30px;"> <input class="form-check-input" type="checkbox" name="pingeveryone" value="on" id="ping_everyone" style="width:30px;height:30px;">
</div> </div>
</div> </div>
</div> </div>
@ -38,7 +38,8 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label>Welcome Message (Max len. 1000)</label> <label>Welcome Message (Max len. 1000)</label>
<textarea name="welcomeMessage" class="form-control" rows="3" style="resize: none">{{.welcomeMessage}}</textarea> <textarea name="welcomeMessage" class="form-control" rows="3" id="welcome_message"
style="resize: none">{{.welcomeMessage}}</textarea>
</div> </div>
</div> </div>
@ -49,10 +50,7 @@
<div class="input-group-prepend"> <div class="input-group-prepend">
<div class="input-group-text">#</div> <div class="input-group-text">#</div>
</div> </div>
<select class="form-control" name="archivechannel"> <select class="form-control" name="archive_channel" id="archive_channel">
{{range .channels}}
<option {{if eq .Id $.archivechannel }}selected{{end}} value="{{.Id}}">{{.Name}}</option>
{{end}}
</select> </select>
</div> </div>
</div> </div>
@ -60,10 +58,7 @@
<div class="col-md-5 px-1"> <div class="col-md-5 px-1">
<div class="form-group"> <div class="form-group">
<label>Channel Category</label> <label>Channel Category</label>
<select class="form-control" name="category"> <select class="form-control" name="category" id="category">
{{range .categories}}
<option {{if eq $.activecategory .Id}}selected{{end}} value="{{.Id}}">{{.Name}}</option>
{{end}}
</select> </select>
</div> </div>
</div> </div>
@ -72,7 +67,8 @@
<div class="form-group"> <div class="form-group">
<label>Allow users to close tickets</label> <label>Allow users to close tickets</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="userscanclose" value="on" {{if .usersCanClose}}checked{{end}} style="width:30px;height:30px;"> <input class="form-check-input" type="checkbox" name="userscanclose" value="on"
id="users_can_close" style="width:30px;height:30px;">
</div> </div>
</div> </div>
</div> </div>
@ -80,23 +76,25 @@
<label>Ticket Naming Scheme</label> <label>Ticket Naming Scheme</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="namingscheme" id="naming-by-id" value="id" {{if eq .namingScheme "id"}}checked{{end}}> <input class="form-check-input" type="radio" name="namingscheme" id="naming-by-id" value="id">
<label class="form-check-label" for="naming-by-id"> <label class="form-check-label white" for="naming-by-id">
Ticket ID Ticket ID (#ticket-1)
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="namingscheme" id="naming-by-username" value="username" {{if eq .namingScheme "username"}}checked{{end}}> <input class="form-check-input" type="radio" name="namingscheme" id="naming-by-username"
<label class="form-check-label" for="naming-by-username"> value="username">
Username <label class="form-check-label white" for="naming-by-username">
Username (#ryan-1)
</label> </label>
</div> </div>
<input name="csrf" type="hidden" value="{{.csrf}}">
<div class="row"> <div class="row">
<div class="col-md-1 pr-1"> <div class="col-md-2 pr-1">
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Submit</button> <button class="btn btn-primary btn-fill" type="submit"><i class="fas fa-paper-plane"></i>
Submit
</button>
</div> </div>
</div> </div>
</div> </div>
@ -107,53 +105,137 @@
</div> </div>
</div> </div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;"> <div aria-live="polite" aria-atomic="true" style="position: relative;">
<div style="position: absolute; right: 10px; min-width: 300px"> <div style="position: absolute; right: 10px" id="toast-container">
{{if .invalidPrefix}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
The prefix you specified was invalid
</div>
</div>
{{end}}
{{if .invalidWelcomeMessage}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
The welcome message you specified was invalid
</div>
</div>
{{end}}
{{if .invalidTicketLimit}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Warning</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
The ticket limit you specified was invalid
</div>
</div>
{{end}}
</div> </div>
</div> </div>
<script> <script>
$('.toast').toast('show'); async function getSettings() {
$('#pingeveryone').prop('indeterminate', {{.pingEveryone}}); const res = await axios.get('/api/{{.guildId}}/settings');
return res.data;
}
async function getChannels() {
const res = await axios.get('/api/{{.guildId}}/channels');
return res.data;
}
async function fillArchiveChannels(channels, archiveChannelId) {
const container = document.getElementById('archive_channel');
channels.filter((ch) => ch.type === 0).forEach((ch) => {
const node = document.createElement("option");
const text = document.createTextNode(ch.name);
node.appendChild(text);
node.value = ch.id;
if (archiveChannelId === ch.id) {
node.selected = true;
}
container.appendChild(node);
});
}
async function fillCategories(channels, categoryId) {
const container = document.getElementById('category');
channels.filter((ch) => ch.type === 4).forEach((ch) => {
const node = document.createElement("option");
const text = document.createTextNode(ch.name);
node.appendChild(text);
node.value = ch.id;
if (categoryId === ch.id) {
node.selected = true;
}
container.appendChild(node);
});
}
async function loadData() {
const settings = await getSettings();
document.getElementById("prefix").value = settings.prefix;
document.getElementById("welcome_message").value = settings.welcome_message;
document.getElementById("ticket_limit").value = settings.ticket_limit;
document.getElementById("ping_everyone").checked = settings.ping_everyone;
document.getElementById("users_can_close").checked = settings.users_can_close;
if (settings.naming_scheme === "username") {
document.getElementById("naming-by-username").checked = true;
} else {
document.getElementById("naming-by-id").checked = true;
}
const channels = await getChannels();
await fillArchiveChannels(channels, settings.archive_channel);
await fillCategories(channels, settings.category);
}
loadData();
</script>
<script>
async function updateSettings() {
const data = {
'prefix': document.getElementById('prefix').value,
'welcome_message': document.getElementById("welcome_message").value,
'ticket_limit': parseInt(document.getElementById('ticket_limit').value),
'ping_everyone': document.getElementById("ping_everyone").checked,
'users_can_close': document.getElementById("users_can_close").checked,
'naming_scheme': document.querySelector('input[name="namingscheme"]:checked').value,
'archive_channel': document.getElementById('archive_channel').options[document.getElementById('archive_channel').selectedIndex].value,
'category': document.getElementById('category').options[document.getElementById('category').selectedIndex].value
};
const res = await axios.post('/api/{{.guildId}}/settings', data);
if (res.status === 200) {
const success = showValidations(res.data);
if (success) {
showToast("Success", "Your settings have been saved.")
}
} else {
showToast("Error", "A severe error occurred. Please check your console for more information.");
console.log(res);
}
}
function showValidations(data) {
let success = true;
if (!data.prefix) {
success = false;
showToast("Warning", "Your prefix has not been saved.<br />Prefixes must be between 1 - 8 characters in length.")
}
if (!data.welcome_message) {
success = false;
showToast("Warning", "Your welcome message has not been saved.<br />Welcome messages must be between 1 - 1000 characters in length.")
}
if (!data.ticket_limit) {
success = false;
showToast("Warning", "Your ticket limit has not been saved.<br />Ticket limits must be in the range 1 - 10.")
}
if (!data.archive_channel) {
success = false;
showToast("Warning", "Your archive channel has not been saved.")
}
if (!data.category) {
success = false;
showToast("Warning", "Your channel category has not been saved.")
}
if (!data.naming_scheme) {
success = false;
showToast("Warning", "Your archive channel has not been saved.")
}
return success;
}
</script> </script>
</div> </div>
{{end}} {{end}}

View File

@ -18,15 +18,7 @@
<th>View</th> <th>View</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="ticket-container">
{{range .tickets}}
<tr>
<td>{{.ticketId}}</td>
<td>{{.username}}#{{.discrim}}</td>
<td>{{range .members}}{{.username}}#{{.discrim}}{{.sep}}{{end}}</td>
<td><a class="btn btn-primary btn-sm" role="button" href="/manage/{{$.guildId}}/tickets/view/{{.uuid}}">View</a></td>
</tr>
{{end}}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -37,7 +29,30 @@
</div> </div>
<script> <script>
$('.toast').toast('show'); function appendTicket(ticket) {
const container = document.getElementById('ticket-container');
const tr = document.createElement('tr');
appendTd(tr, ticket.ticketId);
appendTd(tr, `${ticket.username}#${ticket.discrim}`);
const members = ticket.members.map(member => `${member.username}#${member.discrim}`).join(', ');
appendTd(tr, members);
appendButton(tr, 'View', () => { console.log(ticket); location.href = '/manage/{{.guildId}}/tickets/view/' + ticket.uuid });
container.appendChild(tr);
}
async function loadData() {
const res = await axios.get('/api/{{.guildId}}/tickets');
for (ticket of res.data) {
appendTicket(ticket);
}
}
loadData();
</script> </script>
</div> </div>
{{end}} {{end}}

View File

@ -7,11 +7,10 @@
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Close Ticket</h4> <h4 class="card-title">Close Ticket</h4>
<div class="close-container"> <div class="close-container">
<form class="form-inline" action="/manage/{{.guildId}}/tickets/view/{{.uuid}}/close" method="post"> <form class="form-inline" action="javascript:close()">
<input type="text" class="form-control" id="reason" name="reason" placeholder="No reason specified" style="width: 80%"> <input type="text" class="form-control" id="reason" name="reason" placeholder="No reason specified" style="width: 80%">
<input name="csrf" type="hidden" value="{{.csrf}}">
<div style="padding-left: 10px"> <div style="padding-left: 10px">
<button type="submit" class="btn btn-primary">Close Ticket</button> <button type="submit" class="btn btn-primary btn-fill">Close Ticket</button>
</div> </div>
</form> </form>
</div> </div>
@ -19,25 +18,13 @@
<h4 class="card-title">View Ticket</h4> <h4 class="card-title">View Ticket</h4>
<div class="discord-container"> <div class="discord-container">
<div class="channel-header"> <div class="channel-header">
<span class="channel-name">#ticket-{{.ticketId}}</span> <span id="channel-name"></span>
</div> </div>
<div id="message-container"> <div id="message-container">
{{range .messages}}
<div class="message">
<b>{{.username}}</b>
{{.content}}
</div>
{{end}}
</div> </div>
<div class="input-container"> <div class="input-container">
<form action="javascript:sendMessage()"> <form onsubmit="sendMessage(); return false;">
{{if .premium}} <input type="text" class="form-control message-input" id="message" name="message">
<input type="text" class="form-control message-input" id="message" name="message"
placeholder="Message #ticket-{{.ticketId}}">
{{else}}
<input type="text" class="form-control message-input" id="message" name="message"
placeholder="Premium users get live messages and can respond through webchat" disabled>
{{end}}
</form> </form>
</div> </div>
</div> </div>
@ -47,76 +34,115 @@
</div> </div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;"> <div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;">
<div style="position: absolute; right: 10px; min-width: 300px"> <div style="position: absolute; right: 10px; min-width: 300px" id="toast-container">
{{if .isError}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Error</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
{{.error}}
</div> </div>
</div> </div>
{{end}}
</div> </div>
</div> </div>
<script> <script>
$('.toast').toast('show'); async function close() {
const reason = document.getElementById('reason').value;
document.getElementById('reason').value = '';
const res = await axios.delete('/api/{{.guildId}}/tickets/{{.uuid}}', {
data: {
reason: reason
}
});
if (res.status === 200 && res.data.success) {
location.href = '/manage/{{.guildId}}/tickets';
} else {
showToast('Error', res.data.error);
}
}
</script>
<script>
async function isPremium() {
const res = await axios.get('/api/{{.guildId}}/premium');
return res.data.premium;
}
async function appendMessages(messages) {
const container = document.getElementById('message-container');
for (message of messages) {
const div = document.createElement('div');
div.classList.add('message');
const username = document.createElement('b');
username.appendChild(document.createTextNode(message.username));
username.appendChild(document.createTextNode(': '));
div.appendChild(username);
div.appendChild(document.createTextNode(message.content));
container.appendChild(div);
}
}
async function loadData() {
const premium = await isPremium();
const res = await axios.get('/api/{{.guildId}}/tickets/{{.uuid}}');
if (res.status === 200 && res.data.success) {
const data = res.data;
document.getElementById('channel-name').innerText = `#ticket-${data.ticket.TicketId}`;
await appendMessages(data.messages);
startWebsocket(data.ticket.TicketId);
} else {
showToast('Error', res.data.error);
}
const el = document.getElementById('message');
if (premium) {
el.placeholder = `Message #ticket-${res.data.ticket.TicketId}`;
} else {
el.disabled = true;
el.placeholder = 'Premium users get live messages and can respond through webchat'
}
}
loadData();
</script> </script>
</div>
</div>
<script> <script>
// Scroll to bottom // Scroll to bottom
let container = document.getElementById("message-container"); const container = document.getElementById("message-container");
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
</script>
{{if .premium}} async function startWebsocket(ticketId) {
<script> //const ws = new WebSocket("wss://panel.ticketsbot.net/webchat");
let ws = new WebSocket("wss://panel.ticketsbot.net/webchat"); const ws = new WebSocket("ws://localhost:3000/webchat");
ws.onopen = () => { ws.onopen = () => {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
"type": "auth", "type": "auth",
"data": { "data": {
"guild": "{{.guildId}}", "guild": "{{.guildId}}",
"ticket": "{{.ticketId}}" "ticket": `${ticketId}`
} }
})); }));
}; };
ws.onmessage = (evt) => { ws.onmessage = (evt) => {
let data = JSON.parse(evt.data); const data = JSON.parse(evt.data);
appendMessages([data]);
let container = document.getElementById("message-container");
let element = document.createElement("div");
element.className = "message";
element.innerHTML = `
<b>${data.username}</b>
${data.content}
`;
container.appendChild(element);
// Scroll to bottom
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
}; };
}
function sendMessage() { async function sendMessage() {
let msg = document.getElementById("message").value; const msg = document.getElementById("message").value;
document.getElementById("message").value = ""; document.getElementById("message").value = "";
ws.send(JSON.stringify({ const res = await axios.post('/api/{{.guildId}}/tickets/{{.uuid}}', {message: msg});
"type": "send", if (res.status !== 200 || !res.data.success) {
"data": msg showToast('Error', res.data.error);
})) }
} }
</script> </script>
{{end}} {{end}}
{{end}}

View File

@ -7,7 +7,6 @@ import (
"github.com/TicketsBot/GoPanel/database/table" "github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/rpc/ratelimit" "github.com/TicketsBot/GoPanel/rpc/ratelimit"
"github.com/gin-gonic/contrib/sessions"
gocache "github.com/robfig/go-cache" gocache "github.com/robfig/go-cache"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
"io/ioutil" "io/ioutil"
@ -23,7 +22,7 @@ type ProxyResponse struct {
var premiumCache = gocache.New(10 * time.Minute, 10 * time.Minute) var premiumCache = gocache.New(10 * time.Minute, 10 * time.Minute)
func IsPremiumGuild(store sessions.Session, guildId uint64, ch chan bool) { func IsPremiumGuild(guildId uint64, ch chan bool) {
guildIdRaw := strconv.FormatUint(guildId, 10) guildIdRaw := strconv.FormatUint(guildId, 10)
if premium, ok := premiumCache.Get(guildIdRaw); ok { if premium, ok := premiumCache.Get(guildIdRaw); ok {