Chat replica
This commit is contained in:
parent
34c555dbd4
commit
af3ddaab9e
76
app/http/endpoints/api/transcripts/render.go
Normal file
76
app/http/endpoints/api/transcripts/render.go
Normal 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)
|
||||||
|
}
|
@ -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
50
chatreplica/convert.go
Normal 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
44
chatreplica/proxy.go
Normal 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
55
chatreplica/structs.go
Normal 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"
|
||||||
|
)
|
@ -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"),
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user