diff --git a/app/http/endpoints/api/transcripts/render.go b/app/http/endpoints/api/transcripts/render.go new file mode 100644 index 0000000..0ad8e8b --- /dev/null +++ b/app/http/endpoints/api/transcripts/render.go @@ -0,0 +1,76 @@ +package api + +import ( + "errors" + "github.com/TicketsBot/GoPanel/chatreplica" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/archiverclient" + "github.com/gin-gonic/gin" + "strconv" +) + +func GetTranscriptRenderHandler(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + userId := ctx.Keys["userid"].(uint64) + + // format ticket ID + ticketId, err := strconv.Atoi(ctx.Param("ticketId")) + if err != nil { + ctx.JSON(400, utils.ErrorStr("Invalid ticket ID")) + return + } + + // get ticket object + ticket, err := dbclient.Client.Tickets.Get(ticketId, guildId) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // Verify this is a valid ticket and it is closed + if ticket.UserId == 0 || ticket.Open { + ctx.JSON(404, utils.ErrorStr("Transcript not found")) + return + } + + // Verify the user has permissions to be here + // ticket.UserId cannot be 0 + if ticket.UserId != userId { + hasPermission, err := utils.HasPermissionToViewTicket(guildId, userId, ticket) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if !hasPermission { + ctx.JSON(403, utils.ErrorStr("You do not have permission to view this transcript")) + return + } + } + + // retrieve ticket messages from bucket + messages, err := utils.ArchiverClient.Get(guildId, ticketId) + if err != nil { + if errors.Is(err, archiverclient.ErrExpired) { + ctx.JSON(404, utils.ErrorStr("Transcript not found")) + } else { + ctx.JSON(500, utils.ErrorJson(err)) + } + + return + } + + // Render + payload := chatreplica.FromArchiveMessages(messages, ticketId) + html, err := chatreplica.Render(payload) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + ctx.Data(200, "text/html", html) +} diff --git a/app/http/server.go b/app/http/server.go index 9e1b078..150db3d 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -112,6 +112,7 @@ func StartServer() { // Allow regular users to get their own transcripts, make sure you check perms inside guildApiNoAuth.GET("/transcripts/:ticketId", rl(middleware.RateLimitTypeGuild, 10, 10*time.Second), api_transcripts.GetTranscriptHandler) + guildApiNoAuth.GET("/transcripts/:ticketId/render", rl(middleware.RateLimitTypeGuild, 10, 10*time.Second), api_transcripts.GetTranscriptRenderHandler) guildAuthApiSupport.GET("/tickets", api_ticket.GetTickets) guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket) diff --git a/chatreplica/convert.go b/chatreplica/convert.go new file mode 100644 index 0000000..e87901f --- /dev/null +++ b/chatreplica/convert.go @@ -0,0 +1,50 @@ +package chatreplica + +import ( + "fmt" + "github.com/rxdn/gdl/objects/channel/message" + "strconv" +) + +func FromArchiveMessages(messages []message.Message, ticketId int) Payload { + users := make(map[string]User) + var wrappedMessages []Message // Cannot define length because of continue + + for _, msg := range messages { + // If all 3 are missing, server will 400 + if msg.Content == "" && len(msg.Embeds) == 0 && len(msg.Attachments) == 0 { + continue + } + + wrappedMessages = append(wrappedMessages, Message{ + Id: msg.Id, + Type: msg.Type, + Author: msg.Author.Id, + Time: msg.Timestamp.Unix(), + Content: msg.Content, + Embeds: msg.Embeds, + Attachments: msg.Attachments, + }) + + // Add user to entities map + snowflake := strconv.FormatUint(msg.Author.Id, 10) + if _, ok := users[snowflake]; !ok { + users[snowflake] = User{ + Avatar: msg.Author.AvatarUrl(256), + Username: msg.Author.Username, + Discriminator: msg.Author.Discriminator, + Badge: nil, + } + } + } + + return Payload{ + Entities: Entities{ + Users: users, + Channels: make(map[string]Channel), + Roles: make(map[string]Role), + }, + Messages: wrappedMessages, + ChannelName: fmt.Sprintf("ticket-%d", ticketId), + } +} diff --git a/chatreplica/proxy.go b/chatreplica/proxy.go new file mode 100644 index 0000000..6db99f4 --- /dev/null +++ b/chatreplica/proxy.go @@ -0,0 +1,44 @@ +package chatreplica + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/TicketsBot/GoPanel/config" + "io/ioutil" + "net/http" + "time" +) + +var client = &http.Client{ + Transport: &http.Transport{ + TLSHandshakeTimeout: time.Second * 3, // We're not using TLS anyway + }, + Timeout: time.Second * 3, +} + +func Render(payload Payload) ([]byte, error) { + encoded, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + res, err := client.Post(config.Conf.Bot.RenderServiceUrl, "application/json", bytes.NewBuffer(encoded)) + if err != nil { + return nil, err + } + + fmt.Println(string(encoded)) + + if res.StatusCode != 200 { + return nil, fmt.Errorf("render service returned status code %d", res.StatusCode) + } + + bytes, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return nil, err + } + + return bytes, nil +} diff --git a/chatreplica/structs.go b/chatreplica/structs.go new file mode 100644 index 0000000..85c5320 --- /dev/null +++ b/chatreplica/structs.go @@ -0,0 +1,55 @@ +package chatreplica + +import ( + "github.com/rxdn/gdl/objects/channel" + "github.com/rxdn/gdl/objects/channel/embed" + "github.com/rxdn/gdl/objects/channel/message" + "github.com/rxdn/gdl/objects/user" +) + +type ( + Payload struct { + Entities Entities `json:"entities"` + Messages []Message `json:"messages"` + ChannelName string `json:"channel_name"` + } + + // Entities Snowflake -> Entity map + Entities struct { + Users map[string]User `json:"users"` + Channels map[string]Channel `json:"channels"` + Roles map[string]Role `json:"roles"` + } + + User struct { + Avatar string `json:"avatar"` + Username string `json:"username"` + Discriminator user.Discriminator `json:"discriminator"` + Badge *Badge `json:"badge,omitempty"` + } + + Channel struct { + Name string `json:"name"` + } + + Role struct { + Name string `json:"name"` + Color int `json:"color"` + } + + Message struct { + Id uint64 `json:"id,string"` + Type message.MessageType `json:"type"` + Author uint64 `json:"author,string"` + Time int64 `json:"time"` // Unix seconds + Content string `json:"content"` + Embeds []embed.Embed `json:"embeds,omitempty"` + Attachments []channel.Attachment `json:"attachments,omitempty"` + } +) + +type Badge string + +const ( + BadgeBot Badge = "bot" +) diff --git a/config/config.go b/config/config.go index ba3d69c..4ae0f99 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,7 @@ type ( ObjectStore string AesKey string `toml:"aes-key"` ProxyUrl string `toml:"discord-proxy-url"` + RenderServiceUrl string `toml:"render-service-url"` } Redis struct { @@ -163,6 +164,7 @@ func fromEnvvar() { ObjectStore: os.Getenv("LOG_ARCHIVER_URL"), AesKey: os.Getenv("LOG_AES_KEY"), ProxyUrl: os.Getenv("DISCORD_PROXY_URL"), + RenderServiceUrl: os.Getenv("RENDER_SERVICE_URL"), }, Redis: Redis{ Host: os.Getenv("REDIS_HOST"), diff --git a/envvars.md b/envvars.md index 2925b73..d976b6d 100644 --- a/envvars.md +++ b/envvars.md @@ -28,6 +28,7 @@ - PREMIUM_PROXY_KEY - LOG_ARCHIVER_URL - LOG_AES_KEY +- RENDER_SERVICE_URL - REDIS_HOST - REDIS_PORT - REDIS_PASSWORD diff --git a/frontend/src/views/TranscriptView.svelte b/frontend/src/views/TranscriptView.svelte index c6f0a2e..7ad1d02 100644 --- a/frontend/src/views/TranscriptView.svelte +++ b/frontend/src/views/TranscriptView.svelte @@ -1,33 +1,5 @@ -