Render attachments and embeds
This commit is contained in:
parent
89a5ea3b15
commit
3a7e140314
@ -2,18 +2,18 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TicketsBot/GoPanel/botcontext"
|
||||
"github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
cache2 "github.com/rxdn/gdl/cache"
|
||||
"github.com/rxdn/gdl/objects/channel"
|
||||
"github.com/rxdn/gdl/objects/channel/embed"
|
||||
"github.com/rxdn/gdl/objects/user"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var MentionRegex, _ = regexp.Compile("<@(\\d+)>")
|
||||
@ -41,7 +41,7 @@ func GetTicket(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
// Get the ticket struct
|
||||
ticket, err := database.Client.Tickets.Get(ctx, ticketId, guildId)
|
||||
ticket, err := dbclient.Client.Tickets.Get(ctx, ticketId, guildId)
|
||||
if err != nil {
|
||||
ctx.JSON(500, gin.H{
|
||||
"success": true,
|
||||
@ -77,50 +77,56 @@ func GetTicket(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
messagesFormatted := make([]map[string]interface{}, 0)
|
||||
if ticket.ChannelId != nil {
|
||||
// Get messages
|
||||
messages, err := rest.GetChannelMessages(context.Background(), botContext.Token, botContext.RateLimiter, *ticket.ChannelId, rest.GetChannelMessagesData{Limit: 100})
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Format messages, exclude unneeded data
|
||||
for _, message := range utils.Reverse(messages) {
|
||||
content := message.Content
|
||||
|
||||
// Format mentions properly
|
||||
match := MentionRegex.FindAllStringSubmatch(content, -1)
|
||||
for _, mention := range match {
|
||||
if len(mention) >= 2 {
|
||||
mentionedId, err := strconv.ParseUint(mention[1], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
user, err := cache.Instance.GetUser(context.Background(), mentionedId)
|
||||
if err == nil {
|
||||
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), fmt.Sprintf("@%s", user.Username))
|
||||
} else if errors.Is(err, cache2.ErrNotFound) {
|
||||
content = strings.ReplaceAll(content, fmt.Sprintf("<@%d>", mentionedId), "@Unknown User")
|
||||
} else {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messagesFormatted = append(messagesFormatted, map[string]interface{}{
|
||||
"author": message.Author,
|
||||
"content": content,
|
||||
if ticket.ChannelId == nil {
|
||||
ctx.JSON(404, gin.H{
|
||||
"success": false,
|
||||
"error": "Channel ID is nil",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := fetchMessages(botContext, ticket)
|
||||
if err != nil {
|
||||
ctx.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"ticket": ticket,
|
||||
"messages": messagesFormatted,
|
||||
"messages": messages,
|
||||
})
|
||||
}
|
||||
|
||||
type StrippedMessage struct {
|
||||
Author user.User `json:"author"`
|
||||
Content string `json:"content"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Attachments []channel.Attachment `json:"attachments"`
|
||||
Embeds []embed.Embed `json:"embeds"`
|
||||
}
|
||||
|
||||
func fetchMessages(botContext *botcontext.BotContext, ticket database.Ticket) ([]StrippedMessage, error) {
|
||||
// Get messages
|
||||
messages, err := rest.GetChannelMessages(context.Background(), botContext.Token, botContext.RateLimiter, *ticket.ChannelId, rest.GetChannelMessagesData{Limit: 100})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Format messages, exclude unneeded data
|
||||
stripped := make([]StrippedMessage, len(messages))
|
||||
for i, message := range utils.Reverse(messages) {
|
||||
stripped[i] = StrippedMessage{
|
||||
Author: message.Author,
|
||||
Content: message.Content,
|
||||
Timestamp: message.Timestamp,
|
||||
Attachments: message.Attachments,
|
||||
Embeds: message.Embeds,
|
||||
}
|
||||
}
|
||||
|
||||
return stripped, nil
|
||||
}
|
||||
|
@ -24,14 +24,86 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="discord-container">
|
||||
<section class="discord-container">
|
||||
<div class="channel-header">
|
||||
<span id="channel-name">#ticket-{ticketId}</span>
|
||||
<span class="channel-name">#ticket-{ticketId}</span>
|
||||
</div>
|
||||
<div id="message-container" bind:this={container}>
|
||||
<div class="message-container" bind:this={container}>
|
||||
{#each messages as message}
|
||||
<div class="message">
|
||||
<b>{message.author.username}:</b> {message.content}
|
||||
<img class="avatar" src={getAvatarUrl(message.author.id, message.author.avatar)}
|
||||
on:error={(e) => handleAvatarLoadError(e, message.author.id)} alt="Avatar"/>
|
||||
<div>
|
||||
<div>
|
||||
<span class="username">{message.author.global_name || message.author.username}</span>
|
||||
<span class="timestamp">
|
||||
{new Date() - new Date(message.timestamp) < 86400000 ? getRelativeTime(new Date(message.timestamp)) : new Date(message.timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
{#if message.content?.length > 0}
|
||||
<span class="plaintext">{message.content}</span>
|
||||
{/if}
|
||||
|
||||
{#if message.embeds?.length > 0}
|
||||
<div class="embed-wrapper">
|
||||
{#each message.embeds as embed}
|
||||
<div class="embed">
|
||||
<div class="colour" style="background-color: #{embed.color.toString(16)}"></div>
|
||||
<div class="main">
|
||||
{#if embed.title}
|
||||
<b>{embed.title}</b>
|
||||
{/if}
|
||||
{#if embed.description}
|
||||
<span>{embed.description}</span>
|
||||
{/if}
|
||||
|
||||
{#if embed.fields && embed.fields.length > 0}
|
||||
<div class="fields">
|
||||
{#each embed.fields as field}
|
||||
<div class="field" class:inline={field.inline}>
|
||||
<span class="name">{field.name}</span>
|
||||
<span class="value">{field.value}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if embed.image && embed.image.proxy_url}
|
||||
<img src={embed.image.proxy_url} alt="Embed Image"/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message.attachments?.length > 0}
|
||||
<div class="attachment-wrapper">
|
||||
{#each message.attachments.filter(a => isImage(a.filename)) as attachment}
|
||||
{@const proxyUrl = attachment.proxy_url.replaceAll("\u0026", "&")}
|
||||
<img src={proxyUrl} alt="{attachment.filename}"/>
|
||||
{/each}
|
||||
|
||||
{#each message.attachments.filter(a => !isImage(a.filename)) as attachment}
|
||||
{@const directUrl = attachment.url.replaceAll("\u0026", "&")}
|
||||
{@const proxyUrl = attachment.proxy_url.replaceAll("\u0026", "&")}
|
||||
|
||||
<div class="other">
|
||||
<div class="metadata">
|
||||
<span class="name">{attachment.filename}</span>
|
||||
<span class="size">{formatFileSize(attachment.size)}</span>
|
||||
</div>
|
||||
<a href="{isCdnUrl(directUrl) ? directUrl : proxyUrl}" target="_blank"
|
||||
download="{attachment.filename}">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@ -47,14 +119,16 @@
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {createEventDispatcher, onMount} from "svelte";
|
||||
import {fade} from "svelte/transition";
|
||||
import Button from "./Button.svelte";
|
||||
import Card from "./Card.svelte";
|
||||
import Dropdown from "./form/Dropdown.svelte";
|
||||
import {getAvatarUrl, getDefaultIcon} from "../js/icons";
|
||||
import {getRelativeTime} from "../js/util";
|
||||
|
||||
export let ticketId;
|
||||
export let isPremium = false;
|
||||
@ -92,6 +166,49 @@
|
||||
|
||||
selectedTag = undefined;
|
||||
}
|
||||
|
||||
let failed = [];
|
||||
|
||||
function handleAvatarLoadError(e, userId) {
|
||||
if (!failed.includes(userId)) {
|
||||
failed.push(userId);
|
||||
e.target.src = getDefaultIcon(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function isImage(fileName) {
|
||||
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'gifv', 'webp'];
|
||||
return imageExtensions.includes(fileName.split('.').pop().toLowerCase());
|
||||
}
|
||||
|
||||
function formatFileSize(size) {
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(0)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(0)} MB`;
|
||||
else return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
function isCdnUrl(url) {
|
||||
const parsed = new URL(url);
|
||||
return parsed.hostname === 'cdn.discordapp.com';
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
messages = messages.map(message => {
|
||||
// Sort attachments; image first
|
||||
message.attachments = message.attachments.sort((a, b) => {
|
||||
if (isImage(a.filename) && !isImage(b.filename)) {
|
||||
return -1;
|
||||
} else if (!isImage(a.filename) && isImage(b.filename)) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return message;
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -107,7 +224,8 @@
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Catamaran', sans-serif !important;
|
||||
/*font-family: 'Catamaran', sans-serif !important;*/
|
||||
font-family: 'Poppins', sans-serif !important;
|
||||
}
|
||||
|
||||
.channel-header {
|
||||
@ -123,28 +241,178 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#channel-name {
|
||||
.channel-name {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#message-container {
|
||||
.message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: white !important;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#message-container:last-child {
|
||||
.message:first-child {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.message > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.plaintext {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.embed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
border-radius: 5px;
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
.embed > .colour {
|
||||
width: 4px;
|
||||
border-radius: 5px 0 0 5px;
|
||||
}
|
||||
|
||||
.embed > .main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px 10px 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.field.inline {
|
||||
flex: 0 0 calc(33.3333% - 5px);
|
||||
}
|
||||
|
||||
.field:not(.inline) {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.field > .name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field > .value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.embed > .main > img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.attachment-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
row-gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.attachment-wrapper > img {
|
||||
box-sizing: border-box;
|
||||
width: 40%;
|
||||
min-width: 300px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.attachment-wrapper > .other {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
.attachment-wrapper > .other > .metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.attachment-wrapper > .other > .metadata > .name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.attachment-wrapper > .other > .metadata > .size {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.attachment-wrapper > .other i {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-container:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
@ -6,15 +6,27 @@ export function isAnimated(icon) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getIconUrl(id, icon) {
|
||||
export function getIconUrl(id, icon, size = 256) {
|
||||
if (!icon || icon === "") {
|
||||
return getDefaultIcon(id);
|
||||
}
|
||||
|
||||
if (isAnimated(icon)) {
|
||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.gif?size=256`
|
||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.gif?size=${size}`;
|
||||
} else {
|
||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.webp?size=256`
|
||||
return `https:\/\/cdn.discordapp.com/icons/${id}/${icon}.webp?size=${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAvatarUrl(id, avatar, size = 256) {
|
||||
if (!avatar || avatar === "") {
|
||||
return getDefaultIcon(id);
|
||||
}
|
||||
|
||||
if (isAnimated(avatar)) {
|
||||
return `https:\/\/cdn.discordapp.com/avatars/${id}/${avatar}.gif?size=${size}`;
|
||||
} else {
|
||||
return `https:\/\/cdn.discordapp.com/avatars/${id}/${avatar}.webp?size=${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,22 +6,17 @@
|
||||
<div class="section">
|
||||
<h2 class="section-title">Close Ticket</h2>
|
||||
|
||||
<form on:submit|preventDefault={closeTicket}>
|
||||
<div class="row" style="max-height: 63px; align-items: flex-end"> <!-- hacky -->
|
||||
<div class="col-2" style="margin-bottom: 0 !important;">
|
||||
<Input label="Close Reason" placeholder="No reason specified" col1={true} bind:value={closeReason}/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div style="margin-left: 30px; margin-bottom: 0.5em">
|
||||
<Button danger={true} noShadow icon="fas fa-lock">Close Ticket</Button>
|
||||
<div class="row" style="gap: 20px">
|
||||
<Input label="Close Reason" col2 placeholder="No reason specified" bind:value={closeReason}/>
|
||||
<div style="display: flex; align-items: flex-end; padding-bottom: 8px">
|
||||
<Button danger={true} noShadow icon="fas fa-lock" col3 on:click={closeTicket}>Close Ticket
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2 class="section-title">View Ticket</h2>
|
||||
<DiscordMessages {ticketId} {isPremium} {tags} {messages} bind:container on:send={sendMessage} />
|
||||
<DiscordMessages {ticketId} {isPremium} {tags} {messages} bind:container on:send={sendMessage}/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@ -34,7 +29,7 @@
|
||||
import Button from "../components/Button.svelte";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../js/constants";
|
||||
import {setDefaultHeaders, getToken} from '../includes/Auth.svelte'
|
||||
import {getToken, setDefaultHeaders} from '../includes/Auth.svelte'
|
||||
import Input from "../components/form/Input.svelte";
|
||||
import {navigateTo} from "svelte-router-spa";
|
||||
import DiscordMessages from "../components/DiscordMessages.svelte";
|
||||
|
Loading…
x
Reference in New Issue
Block a user