Chat replica

This commit is contained in:
rxdn 2021-11-26 13:37:00 +00:00
parent 34c555dbd4
commit af3ddaab9e
8 changed files with 235 additions and 39 deletions

View File

@ -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)
}

View File

@ -112,6 +112,7 @@ func StartServer() {
// Allow regular users to get their own transcripts, make sure you check perms inside // 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", 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", api_ticket.GetTickets)
guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket) guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket)

50
chatreplica/convert.go Normal file
View File

@ -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),
}
}

44
chatreplica/proxy.go Normal file
View File

@ -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
}

55
chatreplica/structs.go Normal file
View File

@ -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"
)

View File

@ -61,6 +61,7 @@ type (
ObjectStore string ObjectStore string
AesKey string `toml:"aes-key"` AesKey string `toml:"aes-key"`
ProxyUrl string `toml:"discord-proxy-url"` ProxyUrl string `toml:"discord-proxy-url"`
RenderServiceUrl string `toml:"render-service-url"`
} }
Redis struct { Redis struct {
@ -163,6 +164,7 @@ func fromEnvvar() {
ObjectStore: os.Getenv("LOG_ARCHIVER_URL"), ObjectStore: os.Getenv("LOG_ARCHIVER_URL"),
AesKey: os.Getenv("LOG_AES_KEY"), AesKey: os.Getenv("LOG_AES_KEY"),
ProxyUrl: os.Getenv("DISCORD_PROXY_URL"), ProxyUrl: os.Getenv("DISCORD_PROXY_URL"),
RenderServiceUrl: os.Getenv("RENDER_SERVICE_URL"),
}, },
Redis: Redis{ Redis: Redis{
Host: os.Getenv("REDIS_HOST"), Host: os.Getenv("REDIS_HOST"),

View File

@ -28,6 +28,7 @@
- PREMIUM_PROXY_KEY - PREMIUM_PROXY_KEY
- LOG_ARCHIVER_URL - LOG_ARCHIVER_URL
- LOG_AES_KEY - LOG_AES_KEY
- RENDER_SERVICE_URL
- REDIS_HOST - REDIS_HOST
- REDIS_PORT - REDIS_PORT
- REDIS_PASSWORD - REDIS_PASSWORD

View File

@ -1,33 +1,5 @@
<div class="discord-container"> <iframe srcdoc={html} style="border: none; width: 100%; height: 100%">
<div> </iframe>
<div class="channel-header">
<span class="channel-name">#ticket-{ticketId}</span>
</div>
</div>
<div id="message-container">
{#each messages as message}
<div class="message">
{#if message.timestamp > epoch}
<span class="timestamp">
[{message.timestamp.toLocaleTimeString([], dateFormatSettings)} {message.timestamp.toLocaleDateString()}]
</span>
{/if}
<img src="https://cdn.discordapp.com/avatars/{message.author.id}/{message.author.avatar}.webp?size=256"
class="avatar">
<b class="username">{message.author.username}</b>
<span class="content">{message.content}</span>
{#if message.attachments !== undefined && message.attachments.length > 0}
{#each message.attachments as attachment}
<a href="{attachment.url}" target="_blank" title="{attachment.filename}" class="attachment"><i
class="far fa-file-alt fa-2x"></i></a>
{/each}
{/if}
</div>
{/each}
</div>
</div>
<script> <script>
import axios from "axios"; import axios from "axios";
@ -41,23 +13,18 @@
let guildId = currentRoute.namedParams.id; let guildId = currentRoute.namedParams.id;
let ticketId = currentRoute.namedParams.ticketid; let ticketId = currentRoute.namedParams.ticketid;
setDefaultHeaders() setDefaultHeaders();
let messages = []; let html = '';
let epoch = new Date('2015');
let dateFormatSettings = {
hour: '2-digit',
minute: '2-digit'
};
async function loadData() { async function loadData() {
const res = await axios.get(`${API_URL}/api/${guildId}/transcripts/${ticketId}`); const res = await axios.get(`${API_URL}/api/${guildId}/transcripts/${ticketId}/render`);
if (res.status !== 200) { if (res.status !== 200) {
errorPage(res.data.error); errorPage(res.data.error);
return; return;
} }
messages = res.data.map(message => Object.assign({}, message, {timestamp: new Date(message.timestamp)})); html = res.data;
} }
withLoadingScreen(loadData); withLoadingScreen(loadData);