diff --git a/app/http/endpoints/manage/blacklist.go b/app/http/endpoints/manage/blacklist.go index d542fd5..8f153d5 100644 --- a/app/http/endpoints/manage/blacklist.go +++ b/app/http/endpoints/manage/blacklist.go @@ -56,44 +56,42 @@ func BlacklistHandler(ctx *gin.Context) { blacklistedIds = append(blacklistedIds, user.User) } - usernames := table.GetUsernames(blacklistedIds) + nodes := table.GetUserNodes(blacklistedIds) var blacklisted []map[string]interface{} - for _, node := range blacklistedUsers { + for _, node := range nodes { blacklisted = append(blacklisted, map[string]interface{}{ - "userId": node.User, - "username": usernames[node.User], + "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 - targetIdStr := ctx.Query("userid") - targetId, err := strconv.ParseInt(targetIdStr, 10, 64) + username := ctx.Query("username") + discrim := ctx.Query("discrim") - if err != nil { - userNotFound = true - } else { - // Verify that the user ID is real and in a shared guild - username := table.GetUsername(targetId) - exists := username != "" + // 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 == targetIdStr || table.IsSupport(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": targetIdStr, - "username": username, - }) - } - } + if exists { + if guild.OwnerId == strconv.Itoa(int(targetId)) || table.IsStaff(guildId, targetId) { // Prevent users from blacklisting staff + isStaff = true } else { - userNotFound = true + 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 } } diff --git a/app/http/endpoints/manage/settings.go b/app/http/endpoints/manage/settings.go index d60c095..56c5327 100644 --- a/app/http/endpoints/manage/settings.go +++ b/app/http/endpoints/manage/settings.go @@ -210,6 +210,39 @@ func SettingsHandler(ctx *gin.Context) { } } + panelSettings := table.GetPanelSettings(guildId) + panelUpdated := false + + // Get panel title + panelTitle := ctx.Query("paneltitle") + if panelTitle == "" || len(panelTitle) > 255 || !csrfCorrect { + panelTitle = panelSettings.Title + } else { + panelUpdated = true + } + + // Get panel content + panelContent := ctx.Query("panelcontent") + if panelContent == "" || len(panelContent) > 255 || !csrfCorrect { + panelContent = panelSettings.Content + } else { + panelUpdated = true + } + + // Get panel colour + var panelColour uint64 + panelColourHex := ctx.Query("panelcolour") + if panelColourHex == "" || len(panelColourHex) > 255 || !csrfCorrect { + panelColour = uint64(panelSettings.Colour) + } else { + panelUpdated = true + panelColour, err = strconv.ParseUint(panelColourHex, 16, 32) + } + + if panelUpdated { + go table.UpdatePanelSettings(guildId, panelTitle, panelContent, int(panelColour)) + } + utils.Respond(ctx, template.TemplateSettings.Render(map[string]interface{}{ "name": store.Get("name").(string), "guildId": guildIdStr, @@ -224,6 +257,9 @@ func SettingsHandler(ctx *gin.Context) { "invalidTicketLimit": invalidTicketLimit, "csrf": store.Get("csrf").(string), "pingEveryone": pingEveryone, + "paneltitle": panelTitle, + "panelcontent": panelContent, + "panelcolour": strconv.FormatInt(int64(panelColour), 16), })) } else { ctx.Redirect(302, "/login") diff --git a/app/http/endpoints/manage/ticketlist.go b/app/http/endpoints/manage/ticketlist.go new file mode 100644 index 0000000..743ddb4 --- /dev/null +++ b/app/http/endpoints/manage/ticketlist.go @@ -0,0 +1,110 @@ +package manage + +import ( + "github.com/TicketsBot/GoPanel/app/http/template" + "github.com/TicketsBot/GoPanel/config" + "github.com/TicketsBot/GoPanel/database/table" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/GoPanel/utils/discord/objects" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" + "strconv" + "strings" +) + +func TicketListHandler(ctx *gin.Context) { + store := sessions.Default(ctx) + if store == nil { + return + } + defer store.Save() + + if utils.IsLoggedIn(store) { + userIdStr := store.Get("userid").(string) + userId, err := utils.GetUserId(store) + if err != nil { + ctx.String(500, err.Error()) + return + } + + // Verify the guild exists + guildIdStr := ctx.Param("id") + guildId, err := strconv.ParseInt(guildIdStr, 10, 64) + if err != nil { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page + return + } + + // Get object for selected guild + var guild objects.Guild + for _, g := range table.GetGuilds(userIdStr) { + if g.Id == guildIdStr { + guild = g + break + } + } + + // Verify the user has permissions to be here + if !guild.Owner && !table.IsAdmin(guildId, userId) { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page + return + } + + tickets := table.GetOpenTickets(guildId) + + var toFetch []int64 + for _, ticket := range tickets { + toFetch = append(toFetch, ticket.Owner) + + for _, idStr := range strings.Split(ticket.Members, ",") { + if memberId, err := strconv.ParseInt(idStr, 10, 64); err == nil { + toFetch = append(toFetch, memberId) + } + } + } + + nodes := make(map[int64]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.ParseInt(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": utils.Base64Decode(nodes[memberId].Name), + "discrim": nodes[memberId].Discriminator, + "sep": separator, + }) + } + } + } + + ticketsFormatted = append(ticketsFormatted, map[string]interface{}{ + "uuid": ticket.Uuid, + "ticketId": ticket.TicketId, + "username": utils.Base64Decode(nodes[ticket.Owner].Name), + "discrim": nodes[ticket.Owner].Discriminator, + "members": membersFormatted, + }) + } + + utils.Respond(ctx, template.TemplateTicketList.Render(map[string]interface{}{ + "name": store.Get("name").(string), + "guildId": guildIdStr, + "csrf": store.Get("csrf").(string), + "avatar": store.Get("avatar").(string), + "baseUrl": config.Conf.Server.BaseUrl, + "tickets": ticketsFormatted, + })) + } +} diff --git a/app/http/endpoints/manage/ticketview.go b/app/http/endpoints/manage/ticketview.go new file mode 100644 index 0000000..7e73daa --- /dev/null +++ b/app/http/endpoints/manage/ticketview.go @@ -0,0 +1,98 @@ +package manage + +import ( + "fmt" + "github.com/TicketsBot/GoPanel/app/http/template" + "github.com/TicketsBot/GoPanel/config" + "github.com/TicketsBot/GoPanel/database/table" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/GoPanel/utils/discord/endpoints/channel" + "github.com/TicketsBot/GoPanel/utils/discord/objects" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" + "strconv" +) + +func TicketViewHandler(ctx *gin.Context) { + store := sessions.Default(ctx) + if store == nil { + return + } + defer store.Save() + + if utils.IsLoggedIn(store) { + userIdStr := store.Get("userid").(string) + userId, err := utils.GetUserId(store) + if err != nil { + ctx.String(500, err.Error()) + return + } + + // Verify the guild exists + guildIdStr := ctx.Param("id") + guildId, err := strconv.ParseInt(guildIdStr, 10, 64) + if err != nil { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page + return + } + + // Get object for selected guild + var guild objects.Guild + for _, g := range table.GetGuilds(userIdStr) { + if g.Id == guildIdStr { + guild = g + break + } + } + + // Verify the user has permissions to be here + if !guild.Owner && !table.IsAdmin(guildId, userId) { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page + return + } + + // Get ticket UUID from URL and verify it exists + uuid := ctx.Param("uuid") + ticket := table.GetTicket(uuid) + 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 + } + + // Get messages + var messages []objects.Message + // We want to show users error messages so they can report them + isError := false + var errorMessage string + + endpoint := channel.GetChannelMessages(int(ticket.Channel)) + if err = endpoint.Request(store, nil, nil, &messages); err != nil { + isError = true + errorMessage = err.Error() + } + + // Format messages, exclude unneeded data + var messagesFormatted []map[string]interface{} + for _, message := range utils.Reverse(messages) { + messagesFormatted = append(messagesFormatted, map[string]interface{}{ + "username": message.Author.Username, + "content": message.Content, + }) + } + + utils.Respond(ctx, template.TemplateTicketView.Render(map[string]interface{}{ + "name": store.Get("name").(string), + "guildId": guildIdStr, + "csrf": store.Get("csrf").(string), + "avatar": store.Get("avatar").(string), + "baseUrl": config.Conf.Server.BaseUrl, + "isError": isError, + "error": errorMessage, + "messages": messagesFormatted, + "ticketId": ticket.TicketId, + })) + } +} diff --git a/app/http/server.go b/app/http/server.go index 2df3a52..922b5a7 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -62,6 +62,12 @@ func StartServer() { // /manage/:id/blacklist/remove/:user router.GET("/manage/:id/blacklist/remove/:user", manage.BlacklistRemoveHandler) + // /manage/:id/tickets + router.GET("/manage/:id/tickets", manage.TicketListHandler) + + // /manage/:id/tickets/view/:uuid + //router.GET("/manage/:id/tickets/view/:uuid", manage.TicketViewHandler) + if err := router.Run(config.Conf.Server.Host); err != nil { panic(err) } diff --git a/app/http/template/template.go b/app/http/template/template.go index 6da324e..f8028c4 100644 --- a/app/http/template/template.go +++ b/app/http/template/template.go @@ -22,6 +22,8 @@ var ( TemplateLogs Template TemplateSettings Template TemplateBlacklist Template + TemplateTicketList Template + TemplateTicketView Template ) func (t *Template) Render(context ...interface{}) string { @@ -54,6 +56,14 @@ func LoadTemplates() { compiled: loadTemplate("blacklist"), Layout: LayoutManage, } + TemplateTicketList = Template{ + compiled: loadTemplate("ticketlist"), + Layout: LayoutManage, + } + TemplateTicketView = Template{ + compiled: loadTemplate("ticketview"), + Layout: LayoutManage, + } } func loadLayout(name string) *mustache.Template { diff --git a/database/table/panelsettings.go b/database/table/panelsettings.go new file mode 100644 index 0000000..a094c4b --- /dev/null +++ b/database/table/panelsettings.go @@ -0,0 +1,37 @@ +package table + +import ( + "github.com/TicketsBot/GoPanel/database" +) + +type PanelSettings struct { + GuildId int64 `gorm:"column:GUILDID"` + Title string `gorm:"column:TITLE;type:VARCHAR(255)"` + Content string `gorm:"column:CONTENT;type:TEXT"` + Colour int `gorm:"column:COLOUR` +} + +func (PanelSettings) TableName() string { + return "panelsettings" +} + +func UpdatePanelSettings(guildId int64, title string, content string, colour int) { + settings := PanelSettings{ + Title: title, + Content: content, + Colour: colour, + } + + database.Database.Where(&PanelSettings{GuildId: guildId}).Assign(&settings).FirstOrCreate(&PanelSettings{}) +} + +func GetPanelSettings(guildId int64) PanelSettings { + settings := PanelSettings{ + Title: "Open A Ticket", + Content: "React with :envelope_with_arrow: to open a ticket", + Colour: 2335514, + } + database.Database.Where(PanelSettings{GuildId: guildId}).First(&settings) + + return settings +} diff --git a/database/table/tickets.go b/database/table/tickets.go new file mode 100644 index 0000000..be147e9 --- /dev/null +++ b/database/table/tickets.go @@ -0,0 +1,36 @@ +package table + +import "github.com/TicketsBot/GoPanel/database" + +type Ticket struct { + Uuid string `gorm:"column:UUID;type:varchar(36);primary_key"` + TicketId int `gorm:"column:ID"` + Guild int64 `gorm:"column:GUILDID"` + Channel int64 `gorm:"column:CHANNELID"` + Owner int64 `gorm:"column:OWNERID"` + Members string `gorm:"column:MEMBERS;type:text"` + IsOpen bool `gorm:"column:OPEN"` + OpenTime int64 `gorm:"column:OPENTIME"` +} + +func (Ticket) TableName() string { + return "tickets" +} + +func GetTickets(guild int64) []Ticket { + var tickets []Ticket + database.Database.Where(&Ticket{Guild: guild}).Order("ID asc").Find(&tickets) + return tickets +} + +func GetOpenTickets(guild int64) []Ticket { + var tickets []Ticket + database.Database.Where(&Ticket{Guild: guild, IsOpen: true}).Order("ID asc").Find(&tickets) + return tickets +} + +func GetTicket(uuid string) Ticket { + var ticket Ticket + database.Database.Where(&Ticket{Uuid: uuid}).First(&ticket) + return ticket +} diff --git a/database/table/username.go b/database/table/username.go index 14bf55d..2273dbe 100644 --- a/database/table/username.go +++ b/database/table/username.go @@ -1,13 +1,15 @@ package table import ( - "encoding/base64" "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" ) type UsernameNode struct { Id int64 `gorm:"column:USERID;primary_key"` Name string `gorm:"column:USERNAME;type:text"` // Base 64 encoded + Discriminator string `gorm:"column:DISCRIM;type:varchar(4)"` + Avatar string `gorm:"column:AVATARHASH;type:varchar(100)"` } func (UsernameNode) TableName() string { @@ -17,24 +19,17 @@ func (UsernameNode) TableName() string { func GetUsername(id int64) string { node := UsernameNode{Name: "Unknown"} database.Database.Where(&UsernameNode{Id: id}).First(&node) - return base64Decode(node.Name) + return utils.Base64Decode(node.Name) } -func GetUsernames(ids []int64) map[int64]string { +func GetUserNodes(ids []int64) []UsernameNode { var nodes []UsernameNode database.Database.Where(ids).Find(&nodes) - - m := make(map[int64]string) - for _, node := range nodes { - m[node.Id] = base64Decode(node.Name) - } - - return m + return nodes } -func base64Decode(s string) string { - b, err := base64.StdEncoding.DecodeString(s); if err != nil { - return "" - } - return string(b) +func GetUserId(name, discrim string) int64 { + var node UsernameNode + database.Database.Where(&UsernameNode{Name: utils.Base64Encode(name), Discriminator: discrim}).First(&node) + return node.Id } diff --git a/public/static/css/discordmock.css b/public/static/css/discordmock.css new file mode 100644 index 0000000..61db96c --- /dev/null +++ b/public/static/css/discordmock.css @@ -0,0 +1,31 @@ +@font-face{font-family:Whitney;font-style:light;font-weight:300;src:url('https://discordapp.com/assets/6c6374bad0b0b6d204d8d6dc4a18d820.woff') format('woff')} +@font-face{font-family:Whitney;font-style:normal;font-weight:500;src:url('https://discordapp.com/assets/e8acd7d9bf6207f99350ca9f9e23b168.woff') format('woff')} +@font-face{font-family:Whitney;font-style:medium;font-weight:600;src:url('https://discordapp.com/assets/3bdef1251a424500c1b3a78dea9b7e57.woff') format('woff')} +@font-face{font-family:WhitneyMedium;font-style:medium;font-weight:600;src:url('https://discordapp.com/assets/be0060dafb7a0e31d2a1ca17c0708636.woff') format('woff')} +@font-face{font-family:Whitney;font-style:bold;font-weight:700;src:url('https://discordapp.com/assets/8e12fb4f14d9c4592eb8ec9f22337b04.woff') format('woff')} + +.discord-container { + background-color: #2e3136; + border-radius: 25px; + height: 100vh; + margin: 0; + padding: 0; + font-family: 'Whitney', sans-serif !important; +} + +.channel-header { + background-color: #1e2124; + height: 5vh; + width: 100%; + border-radius: 25px 25px 0 0; + position: relative; + + text-align: center; + display: flex; + align-items: center; +} + +.channel-name { + color: white; + padding-left: 20px; +} diff --git a/public/templates/layouts/manage.mustache b/public/templates/layouts/manage.mustache index 2bdeef9..41218f0 100644 --- a/public/templates/layouts/manage.mustache +++ b/public/templates/layouts/manage.mustache @@ -62,6 +62,10 @@ + + + + @@ -112,6 +116,9 @@