diff --git a/app/http/endpoints/manage/logs.go b/app/http/endpoints/manage/logs.go deleted file mode 100644 index 81721fe..0000000 --- a/app/http/endpoints/manage/logs.go +++ /dev/null @@ -1,102 +0,0 @@ -package manage - -import ( - "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 LogsHandler(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 - } - - 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 logs - // Get user ID from URL - var filteredUserId uint64 - if utils.IsInt(ctx.Query("userid")) { - filteredUserId, _ = strconv.ParseUint(ctx.Query("userid"), 10, 64) - } - - // Get ticket ID from URL - var ticketId int - if utils.IsInt(ctx.Query("ticketid")) { - ticketId, _ = strconv.Atoi(ctx.Query("ticketid")) - } - - // Get username from URL - username := ctx.Query("username") - - // Get logs from DB - logs := table.GetFilteredTicketArchives(guildId, filteredUserId, username, ticketId) - - // Select 30 logs + format them - var formattedLogs []map[string]interface{} - for i := (page - 1) * pageLimit; i < (page - 1) * pageLimit + pageLimit; i++ { - if i >= len(logs) { - break - } - - log := logs[i] - formattedLogs = append(formattedLogs, map[string]interface{}{ - "ticketid": log.TicketId, - "userid": log.User, - "username": log.Username, - "uuid": log.Uuid, - }) - } - - ctx.HTML(200, "manage/logs",gin.H{ - "name": store.Get("name").(string), - "guildId": guildIdStr, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - "isPageOne": page == 1, - "previousPage": page - 1, - "nextPage": page + 1, - "logs": formattedLogs, - "page": page, - }) - } else { - ctx.Redirect(302, "/login") - } -} diff --git a/app/http/endpoints/manage/logslist.go b/app/http/endpoints/manage/logslist.go new file mode 100644 index 0000000..68d23d6 --- /dev/null +++ b/app/http/endpoints/manage/logslist.go @@ -0,0 +1,153 @@ +package manage + +import ( + "context" + "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/gin" + "github.com/rxdn/gdl/rest" + "strconv" +) + +func LogsHandler(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 + } + + 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 "data"->>'Username'=$1 and exists(SELECT FROM members where members.guild_id=$2);`, 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{ + "name": store.Get("name").(string), + "guildId": guildIdStr, + "avatar": store.Get("avatar").(string), + "baseUrl": config.Conf.Server.BaseUrl, + "isPageOne": page == 1, + "previousPage": page - 1, + "nextPage": page + 1, + "logs": formattedLogs, + "page": page, + }) + } else { + ctx.Redirect(302, "/login") + } +} diff --git a/app/http/endpoints/manage/logsview.go b/app/http/endpoints/manage/logsview.go new file mode 100644 index 0000000..169094b --- /dev/null +++ b/app/http/endpoints/manage/logsview.go @@ -0,0 +1,86 @@ +package manage + +import ( + "errors" + "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/TicketsBot/archiverclient" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" + "strconv" +) + +var Archiver archiverclient.ArchiverClient + +func LogViewHandler(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 + guildId, err := strconv.ParseUint(ctx.Param("id"), 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) + + // format ticket ID + ticketId, err := strconv.Atoi(ctx.Param("ticket")); if err != nil { + ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/page/1", guild.Id)) + return + } + + // get ticket object + ticketChan := make(chan table.Ticket) + go table.GetTicketById(guildId, ticketId, ticketChan) + ticket := <-ticketChan + + // Verify this is a valid ticket and it is closed + if ticket.Uuid == "" || ticket.IsOpen { + ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/page/1", guild.Id)) + return + } + + // Verify the user has permissions to be here + isAdmin := make(chan bool) + go utils.IsAdmin(guild, userId, isAdmin) + if !<-isAdmin && ticket.Owner != userId { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page + return + } + + // retrieve ticket messages from bucket + messages, err := Archiver.Get(guildId, ticketId) + if err != nil { + if errors.Is(err, archiverclient.ErrExpired) { + ctx.String(200, "Archives expired: Purchase premium for permanent log storage") // TODO: Actual error page + return + } + + ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error())) + return + } + + // format to html + html, err := Archiver.Encode(messages, ticketId) + if err != nil { + ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error())) + return + } + + ctx.Data(200, gin.MIMEHTML, html) + } else { + ctx.Redirect(302, fmt.Sprintf("/login?noguilds&state=viewlog.%s.%s", ctx.Param("id"), ctx.Param("ticket"))) + } +} diff --git a/app/http/endpoints/manage/viewlog.go b/app/http/endpoints/manage/viewlog.go deleted file mode 100644 index 1ac27cb..0000000 --- a/app/http/endpoints/manage/viewlog.go +++ /dev/null @@ -1,79 +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" - "io/ioutil" - "net/http" - "strconv" - "time" -) - -func LogViewHandler(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 - } - - uuid := ctx.Param("uuid") - - // Doesn't need guild = ticket.guild check, since we select where uuid=uuid and guild=guild - cdnUrl := table.GetCdnUrl(guildId, uuid) - - if cdnUrl == "" { - ctx.Redirect(302, fmt.Sprintf("/manage/%s/logs/page/1", guild.Id)) - return - } else { - req, err := http.NewRequest("GET", cdnUrl, nil); if err != nil { - ctx.String(500, fmt.Sprintf("Failed to read log: %s", err.Error())) - return - } - - client := &http.Client{} - client.Timeout = 3 * time.Second - - res, err := client.Do(req); if err != nil { - ctx.String(500, fmt.Sprintf("Failed to read log: %s", err.Error())) - return - } - defer res.Body.Close() - - content, err := ioutil.ReadAll(res.Body); if err != nil { - ctx.String(500, fmt.Sprintf("Failed to read log: %s", err.Error())) - return - } - - ctx.String(200, string(content)) - } - } else { - ctx.Redirect(302, "/login") - } -} diff --git a/app/http/endpoints/root/callback.go b/app/http/endpoints/root/callback.go index ba03ea9..0faa5ca 100644 --- a/app/http/endpoints/root/callback.go +++ b/app/http/endpoints/root/callback.go @@ -14,6 +14,7 @@ import ( "github.com/gin-gonic/gin" "github.com/rxdn/gdl/objects/guild" "github.com/rxdn/gdl/objects/user" + "strings" "time" ) @@ -48,8 +49,8 @@ func CallbackHandler(ctx *gin.Context) { return } - code := ctx.DefaultQuery("code", "") - if code == "" { + code, ok := ctx.GetQuery("code") + if !ok { ctx.String(400, "Discord provided an invalid Oauth2 code") return } @@ -80,7 +81,7 @@ func CallbackHandler(ctx *gin.Context) { log.Error(err.Error()) } - ctx.Redirect(302, config.Conf.Server.BaseUrl) + handleRedirect(ctx) // Cache guilds because Discord takes like 2 whole seconds to return then go func() { @@ -109,4 +110,14 @@ func CallbackHandler(ctx *gin.Context) { // TODO: unfuck this table.UpdateGuilds(currentUser.Id, base64.StdEncoding.EncodeToString(marshalled)) }() -} \ No newline at end of file +} + +func handleRedirect(ctx *gin.Context) { + state := strings.Split(ctx.Query("state"), ".") + + if len(state) == 3 && state[0] == "viewlog" { + ctx.Redirect(302, fmt.Sprintf("%s/manage/%s/logs/view/%s", config.Conf.Server.BaseUrl, state[1], state[2])) + } else { + ctx.Redirect(302, config.Conf.Server.BaseUrl) + } +} diff --git a/app/http/endpoints/root/login.go b/app/http/endpoints/root/login.go index 974cf4c..ea311b8 100644 --- a/app/http/endpoints/root/login.go +++ b/app/http/endpoints/root/login.go @@ -1,25 +1,31 @@ -package root - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/TicketsBot/GoPanel/utils" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "net/url" -) - -func LoginHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { - return - } - defer store.Save() - - if utils.IsLoggedIn(store) { - ctx.Redirect(302, config.Conf.Server.BaseUrl) - } else { - redirect := url.QueryEscape(config.Conf.Oauth.RedirectUri) - ctx.Redirect(302, fmt.Sprintf("https://discordapp.com/oauth2/authorize?response_type=code&redirect_uri=%s&scope=identify+guilds&client_id=%d", redirect, config.Conf.Oauth.Id)) - } -} +package root + +import ( + "fmt" + "github.com/TicketsBot/GoPanel/config" + "github.com/TicketsBot/GoPanel/utils" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" + "net/url" +) + +func LoginHandler(ctx *gin.Context) { + store := sessions.Default(ctx) + if store == nil { + return + } + defer store.Save() + + if utils.IsLoggedIn(store) { + ctx.Redirect(302, config.Conf.Server.BaseUrl) + } else { + redirect := url.QueryEscape(config.Conf.Oauth.RedirectUri) + + var guildsScope string + if _, noGuilds := ctx.GetQuery("noguilds"); !noGuilds { + guildsScope = "+guilds" + } + + ctx.Redirect(302, fmt.Sprintf("https://discordapp.com/oauth2/authorize?response_type=code&redirect_uri=%s&scope=identify%s&client_id=%d&state=%s", redirect, guildsScope, config.Conf.Oauth.Id, ctx.Query("state"))) + } +} diff --git a/app/http/server.go b/app/http/server.go index 1eece7d..c463480 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -44,7 +44,7 @@ func StartServer() { router.POST("/manage/:id/settings", manage.UpdateSettingsHandler) router.GET("/manage/:id/logs/page/:page", manage.LogsHandler) - router.GET("/manage/:id/logs/view/:uuid", manage.LogViewHandler) + router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) router.GET("/manage/:id/blacklist", manage.BlacklistHandler) router.GET("/manage/:id/blacklist/remove/:user", manage.BlacklistRemoveHandler) diff --git a/cmd/panel/main.go b/cmd/panel/main.go index 8e34cb1..4226179 100644 --- a/cmd/panel/main.go +++ b/cmd/panel/main.go @@ -12,6 +12,7 @@ import ( "github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/rpc/ratelimit" "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/archiverclient" "github.com/apex/log" gdlratelimit "github.com/rxdn/gdl/rest/ratelimit" "math/rand" @@ -32,12 +33,14 @@ func main() { database.ConnectToDatabase() cache.Instance = cache.NewCache() + manage.Archiver = archiverclient.NewArchiverClient(config.Conf.Bot.ObjectStore) + utils.LoadEmoji() messagequeue.Client = messagequeue.NewRedisClient() go Listen(messagequeue.Client) - ratelimit.Ratelimiter = gdlratelimit.NewRateLimiter(gdlratelimit.NewRedisStore(messagequeue.Client.Client, "ratelimit")) // TODO: Use values from config + ratelimit.Ratelimiter = gdlratelimit.NewRateLimiter(gdlratelimit.NewRedisStore(messagequeue.Client.Client, "ratelimit"), 1) // TODO: Use values from config http.StartServer() } diff --git a/config/config.go b/config/config.go index 8972071..be0dcf3 100644 --- a/config/config.go +++ b/config/config.go @@ -52,6 +52,7 @@ type ( Token string PremiumLookupProxyUrl string `toml:"premium-lookup-proxy-url"` PremiumLookupProxyKey string `toml:"premium-lookup-proxy-key"` + ObjectStore string } Redis struct { diff --git a/database/table/tickets.go b/database/table/tickets.go index 3174ea7..885430a 100644 --- a/database/table/tickets.go +++ b/database/table/tickets.go @@ -10,7 +10,7 @@ type Ticket struct { Owner uint64 `gorm:"column:OWNERID"` Members string `gorm:"column:MEMBERS;type:text"` IsOpen bool `gorm:"column:OPEN"` - OpenTime int64 `gorm:"column:OPENTIME"` + OpenTime int64 `gorm:"column:OPENTIME"` } func (Ticket) TableName() string { @@ -23,6 +23,18 @@ func GetTickets(guild uint64) []Ticket { return tickets } +func GetClosedTickets(guildId uint64) []Ticket { + var tickets []Ticket + database.Database.Where(&Ticket{Guild: guildId, IsOpen: false}).Order("ID asc").Find(&tickets) + return tickets +} + +func GetClosedTicketsByUserId(guildId uint64, userIds []uint64) []Ticket { + var tickets []Ticket + database.Database.Where(&Ticket{Guild: guildId, IsOpen: false}).Where("OWNERID IN (?)", userIds).Order("ID asc").Find(&tickets) + return tickets +} + func GetOpenTickets(guild uint64) []Ticket { var tickets []Ticket database.Database.Where(&Ticket{Guild: guild, IsOpen: true}).Order("ID asc").Find(&tickets) diff --git a/go.mod b/go.mod index b9f0abc..9aec99c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/BurntSushi/toml v0.3.1 + github.com/TicketsBot/archiverclient v0.0.0-20200417174514-cf009e9a2547 github.com/apex/log v1.1.2 github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/gin-contrib/multitemplate v0.0.0-20200226145339-3e397ee01bc6 @@ -19,6 +20,5 @@ require ( github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c github.com/pkg/errors v0.9.1 github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 - github.com/rxdn/gdl v0.0.0-20200410134146-c1b0871088c5 - github.com/vmihailenco/msgpack v4.0.4+incompatible + github.com/rxdn/gdl v0.0.0-20200417164852-76b2d3c847c1 ) diff --git a/public/templates/views/logs.tmpl b/public/templates/views/logs.tmpl index cb91ef5..5433103 100644 --- a/public/templates/views/logs.tmpl +++ b/public/templates/views/logs.tmpl @@ -70,7 +70,7 @@