diff --git a/app/http/endpoints/api/getticket.go b/app/http/endpoints/api/getticket.go index 9447df1..698c656 100644 --- a/app/http/endpoints/api/getticket.go +++ b/app/http/endpoints/api/getticket.go @@ -4,6 +4,7 @@ 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/gin" @@ -56,9 +57,8 @@ func GetTicket(ctx *gin.Context) { continue } - ch := make(chan string) - go table.GetUsername(mentionedId, ch) - content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", <-ch)) + user, _ := cache.Instance.GetUser(mentionedId) + content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", user.Username)) } } diff --git a/app/http/endpoints/api/modmaillogslist.go b/app/http/endpoints/api/modmaillogslist.go new file mode 100644 index 0000000..0b33087 --- /dev/null +++ b/app/http/endpoints/api/modmaillogslist.go @@ -0,0 +1,81 @@ +package api + +import ( + "context" + "fmt" + "github.com/TicketsBot/GoPanel/database/table" + "github.com/TicketsBot/GoPanel/rpc/cache" + "github.com/gin-gonic/gin" + "regexp" + "strconv" +) + +var modmailPathRegex = regexp.MustCompile(`(\d+)\/modmail\/(?:free-)?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})`) + +type wrappedModLog struct { + Uuid string `json:"uuid"` + GuildId uint64 `json:"guild_id,string"` + UserId uint64 `json:"user_id,string"` +} + +func GetModmailLogs(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + + page, err := strconv.Atoi(ctx.Param("page")) + if err != nil { + ctx.AbortWithStatusJSON(400, gin.H{ + "success": false, + "error": err.Error(), + }) + } + + // filter + var userId uint64 + + if userIdRaw, filterByUserId := ctx.GetQuery("userid"); filterByUserId { + userId, err = strconv.ParseUint(userIdRaw, 10, 64) + if err != nil { + ctx.AbortWithStatusJSON(400, gin.H{ + "success": false, + "error": "Invalid user ID", + }) + return + } + } else if username, filterByUsername := ctx.GetQuery("username"); filterByUsername { + if err := cache.Instance.QueryRow(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).Scan(&userId); err != nil { + ctx.AbortWithStatusJSON(404, gin.H{ + "success": false, + "error": "User not found", + }) + return + } + } + + shouldFilter := userId > 0 + + start := pageLimit * (page - 1) + end := start + pageLimit - 1 + + wrapped := make([]wrappedModLog, 0) + + var archives []table.ModMailArchive + if shouldFilter { + archivesCh := make(chan []table.ModMailArchive) + go table.GetModmailArchivesByUser(userId, guildId, archivesCh) + archives = <-archivesCh + } else { + archivesCh := make(chan []table.ModMailArchive) + go table.GetModmailArchivesByGuild(guildId, archivesCh) + archives = <-archivesCh + } + + for i := start; i < end && i < len(archives); i++ { + wrapped = append(wrapped, wrappedModLog{ + Uuid: archives[i].Uuid, + GuildId: archives[i].Guild, + UserId: archives[i].User, + }) + } + + ctx.JSON(200, wrapped) +} diff --git a/app/http/endpoints/api/user.go b/app/http/endpoints/api/user.go new file mode 100644 index 0000000..5419742 --- /dev/null +++ b/app/http/endpoints/api/user.go @@ -0,0 +1,36 @@ +package api + +import ( + "context" + "github.com/TicketsBot/GoPanel/rpc/cache" + "github.com/gin-gonic/gin" + "strconv" +) + +func UserHandler(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": "Invalid user ID", + }) + return + } + + var username string + if err := cache.Instance.QueryRow(context.Background(), `SELECT "data"->>'Username' FROM users WHERE users.user_id=$1 AND EXISTS(SELECT FROM members WHERE members.guild_id=$2);`, userId, guildId).Scan(&username); err != nil { + ctx.JSON(404, gin.H{ + "success": false, + "error": "Not found", + }) + return + } + + ctx.JSON(200, gin.H{ + "user_id": userId, + "guild_id": guildId, + "username": username, + }) +} diff --git a/app/http/endpoints/manage/logsview.go b/app/http/endpoints/manage/logsview.go index 7c7984a..56eb7be 100644 --- a/app/http/endpoints/manage/logsview.go +++ b/app/http/endpoints/manage/logsview.go @@ -67,14 +67,14 @@ func LogViewHandler(ctx *gin.Context) { return } - ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error())) + ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error())) return } // format to html - html, err := Archiver.Encode(messages, ticketId) + html, err := Archiver.Encode(messages, fmt.Sprintf("ticket-%d", ticketId)) if err != nil { - ctx.String(500, fmt.Sprintf("Failed to archives - please contact the developers: %s", err.Error())) + ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error())) return } diff --git a/app/http/endpoints/manage/modmaillogslist.go b/app/http/endpoints/manage/modmaillogslist.go new file mode 100644 index 0000000..c7f20a1 --- /dev/null +++ b/app/http/endpoints/manage/modmaillogslist.go @@ -0,0 +1,19 @@ +package manage + +import ( + "github.com/TicketsBot/GoPanel/config" + "github.com/gin-gonic/contrib/sessions" + "github.com/gin-gonic/gin" +) + +func ModmailLogsHandler(ctx *gin.Context) { + store := sessions.Default(ctx) + guildId := ctx.Keys["guildid"].(uint64) + + ctx.HTML(200, "manage/modmaillogs", gin.H{ + "name": store.Get("name").(string), + "guildId": guildId, + "avatar": store.Get("avatar").(string), + "baseUrl": config.Conf.Server.BaseUrl, + }) +} diff --git a/app/http/endpoints/manage/modmaillogsview.go b/app/http/endpoints/manage/modmaillogsview.go new file mode 100644 index 0000000..f0906fd --- /dev/null +++ b/app/http/endpoints/manage/modmaillogsview.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" +) + +func ModmailLogViewHandler(ctx *gin.Context) { + store := sessions.Default(ctx) + if store == nil { + return + } + + 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) + + // get ticket UUID + uuid := ctx.Param("uuid") + + // get ticket object + archiveCh := make(chan table.ModMailArchive) + go table.GetModmailArchive(uuid, archiveCh) + archive := <-archiveCh + + // Verify this is a valid ticket and it is closed + if archive.Uuid == "" { + ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/modmail", guild.Id)) + return + } + + // Verify this modmail ticket was for this guild + if archive.Guild != guildId { + ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs/modmail", guild.Id)) + return + } + + // Verify the user has permissions to be here + isAdmin := make(chan bool) + go utils.IsAdmin(guild, userId, isAdmin) + if !<-isAdmin && archive.User != userId { + ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page + return + } + + // retrieve ticket messages from bucket + messages, err := Archiver.GetModmail(guildId, uuid) + 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 retrieve archive - please contact the developers: %s", err.Error())) + return + } + + // format to html + html, err := Archiver.Encode(messages, fmt.Sprintf("modmail-%s", uuid)) + if err != nil { + ctx.String(500, fmt.Sprintf("Failed to retrieve archive - 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/server.go b/app/http/server.go index 2d3b0e9..6034190 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -11,7 +11,11 @@ import ( "github.com/gin-contrib/static" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" + "github.com/ulule/limiter/v3" + mgin "github.com/ulule/limiter/v3/drivers/middleware/gin" + "github.com/ulule/limiter/v3/drivers/store/memory" "log" + "time" ) func StartServer() { @@ -34,6 +38,7 @@ func StartServer() { router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false))) router.Use(gin.Recovery()) + router.Use(createLimiter()) // Register templates router.HTMLRender = createRenderer() @@ -42,6 +47,7 @@ func StartServer() { router.GET("/callback", root.CallbackHandler) router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect + router.GET("/manage/:id/logs/modmail/view/:uuid", manage.ModmailLogViewHandler) // we check in the actual handler bc of a custom redirect authorized := router.Group("/", middleware.AuthenticateCookie) { @@ -54,6 +60,7 @@ func StartServer() { authenticateGuild.GET("/manage/:id/settings", manage.SettingsHandler) authenticateGuild.GET("/manage/:id/logs", manage.LogsHandler) + authenticateGuild.GET("/manage/:id/logs/modmail", manage.ModmailLogsHandler) authenticateGuild.GET("/manage/:id/blacklist", manage.BlacklistHandler) authenticateGuild.GET("/manage/:id/panels", manage.PanelHandler) @@ -69,6 +76,7 @@ func StartServer() { { guildAuthApi.GET("/:id/channels", api.ChannelsHandler) guildAuthApi.GET("/:id/premium", api.PremiumHandler) + guildAuthApi.GET("/:id/user/:user", api.UserHandler) guildAuthApi.GET("/:id/settings", api.GetSettingsHandler) guildAuthApi.POST("/:id/settings", api.UpdateSettingsHandler) @@ -82,6 +90,7 @@ func StartServer() { guildAuthApi.DELETE("/:id/panels/:message", api.DeletePanel) guildAuthApi.GET("/:id/logs/:page", api.GetLogs) + guildAuthApi.GET("/:id/modmail/logs/:page", api.GetModmailLogs) guildAuthApi.GET("/:id/tickets", api.GetTickets) guildAuthApi.GET("/:id/tickets/:uuid", api.GetTicket) @@ -106,6 +115,7 @@ func createRenderer() multitemplate.Renderer { r = addManageTemplate(r, "blacklist") r = addManageTemplate(r, "logs") + r = addManageTemplate(r, "modmaillogs") r = addManageTemplate(r, "settings") r = addManageTemplate(r, "ticketlist") r = addManageTemplate(r, "ticketview") @@ -135,3 +145,12 @@ func addManageTemplate(renderer multitemplate.Renderer, name string) multitempla return renderer } +func createLimiter() func(*gin.Context) { + store := memory.NewStore() + rate := limiter.Rate{ + Period: time.Minute * 10, + Limit: 600, + } + + return mgin.NewMiddleware(limiter.New(store, rate)) +} diff --git a/cmd/panel/main.go b/cmd/panel/main.go index 4226179..4c6377d 100644 --- a/cmd/panel/main.go +++ b/cmd/panel/main.go @@ -33,7 +33,7 @@ func main() { database.ConnectToDatabase() cache.Instance = cache.NewCache() - manage.Archiver = archiverclient.NewArchiverClient(config.Conf.Bot.ObjectStore) + manage.Archiver = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second * 15) utils.LoadEmoji() diff --git a/database/table/modmailarchive.go b/database/table/modmailarchive.go new file mode 100644 index 0000000..949c534 --- /dev/null +++ b/database/table/modmailarchive.go @@ -0,0 +1,39 @@ +package table + +import ( + "github.com/TicketsBot/GoPanel/database" + "time" +) + +type ModMailArchive struct { + Uuid string `gorm:"column:UUID;type:varchar(36);unique;primary_key"` + Guild uint64 `gorm:"column:GUILDID"` + User uint64 `gorm:"column:USERID"` + CloseTime time.Time `gorm:"column:CLOSETIME"` +} + +func (ModMailArchive) TableName() string { + return "modmail_archive" +} + +func (m *ModMailArchive) Store() { + database.Database.Create(m) +} + +func GetModmailArchive(uuid string, ch chan ModMailArchive) { + var row ModMailArchive + database.Database.Where(ModMailArchive{Uuid: uuid}).Take(&row) + ch <- row +} + +func GetModmailArchivesByUser(userId, guildId uint64, ch chan []ModMailArchive) { + var rows []ModMailArchive + database.Database.Where(ModMailArchive{User: userId, Guild: guildId}).Order("CLOSETIME desc").Find(&rows) + ch <- rows +} + +func GetModmailArchivesByGuild(guildId uint64, ch chan []ModMailArchive) { + var rows []ModMailArchive + database.Database.Where(ModMailArchive{Guild: guildId}).Order("CLOSETIME desc").Find(&rows) + ch <- rows +} diff --git a/database/table/username.go b/database/table/username.go deleted file mode 100644 index e848d43..0000000 --- a/database/table/username.go +++ /dev/null @@ -1,34 +0,0 @@ -package table - -import ( - "github.com/TicketsBot/GoPanel/database" -) - -type UsernameNode struct { - Id uint64 `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 { - return "usernames" -} - -func GetUsername(id uint64, ch chan string) { - node := UsernameNode{Name: "Unknown"} - database.Database.Where(&UsernameNode{Id: id}).First(&node) - ch <- node.Name -} - -func GetUserNodes(ids []uint64) []UsernameNode { - var nodes []UsernameNode - database.Database.Where(ids).Find(&nodes) - return nodes -} - -func GetUserId(name, discrim string) uint64 { - var node UsernameNode - database.Database.Where(&UsernameNode{Name: name, Discriminator: discrim}).First(&node) - return node.Id -} diff --git a/go.mod b/go.mod index f5d787a..1035b21 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/BurntSushi/toml v0.3.1 - github.com/TicketsBot/archiverclient v0.0.0-20200420161043-3532ff9ea943 + github.com/TicketsBot/archiverclient v0.0.0-20200425115930-0ca198cc8306 github.com/apex/log v1.1.2 github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible @@ -21,6 +21,7 @@ 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-20200417164852-76b2d3c847c1 + github.com/rxdn/gdl v0.0.0-20200421193445-f200b9f466d7 + github.com/ulule/limiter/v3 v3.5.0 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a ) diff --git a/public/templates/includes/navbar.tmpl b/public/templates/includes/navbar.tmpl index b58c103..2adaf2b 100644 --- a/public/templates/includes/navbar.tmpl +++ b/public/templates/includes/navbar.tmpl @@ -7,6 +7,9 @@