244 lines
5.4 KiB
Go
244 lines
5.4 KiB
Go
package transcript
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
"github.com/leolionad58/ticketbot/internal/db"
|
|
)
|
|
|
|
const maxUploadSize = 25 * 1024 * 1024 // 25MB
|
|
|
|
type Message struct {
|
|
AuthorName string
|
|
AuthorAvatar string
|
|
AuthorInitial string
|
|
Content template.HTML
|
|
Timestamp string
|
|
Attachments []Attachment
|
|
Embeds []RenderedEmbed
|
|
}
|
|
|
|
type Attachment struct {
|
|
Name string
|
|
URL string
|
|
}
|
|
|
|
type RenderedEmbed struct {
|
|
Color template.CSS // CSS hex e.g. "#5865f2"
|
|
AuthorName string
|
|
AuthorIcon string
|
|
AuthorURL string
|
|
Title string
|
|
TitleURL string
|
|
Description template.HTML
|
|
Fields []EmbedField
|
|
ImageURL string
|
|
ThumbURL string
|
|
FooterText string
|
|
FooterIcon string
|
|
Timestamp string
|
|
Empty bool // true if nothing visible to show
|
|
}
|
|
|
|
type EmbedField struct {
|
|
Name string
|
|
Value template.HTML
|
|
Inline bool
|
|
}
|
|
|
|
type Generator struct {
|
|
OutputDir string
|
|
Session *discordgo.Session
|
|
}
|
|
|
|
func NewGenerator(outputDir string, s *discordgo.Session) *Generator {
|
|
return &Generator{OutputDir: outputDir, Session: s}
|
|
}
|
|
|
|
func (g *Generator) Generate(ticket *db.Ticket) (string, error) {
|
|
messages, err := g.fetchAll(ticket.ChannelID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetch messages: %w", err)
|
|
}
|
|
|
|
rendered := make([]Message, 0, len(messages))
|
|
for _, m := range messages {
|
|
rendered = append(rendered, renderMessage(m))
|
|
}
|
|
|
|
tmpl, err := template.New("transcript").Parse(HTMLTemplate)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse template: %w", err)
|
|
}
|
|
|
|
data := struct {
|
|
Ticket *db.Ticket
|
|
Messages []Message
|
|
Generated string
|
|
}{
|
|
Ticket: ticket,
|
|
Messages: rendered,
|
|
Generated: time.Now().UTC().Format("02/01/2006 à 15:04 UTC"),
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return "", fmt.Errorf("render template: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(g.OutputDir, 0755); err != nil {
|
|
return "", fmt.Errorf("create output dir: %w", err)
|
|
}
|
|
|
|
outPath := filepath.Join(g.OutputDir,
|
|
fmt.Sprintf("%s-%04d-%s.html", ticket.Type, ticket.TicketNumber, ticket.ChannelID))
|
|
|
|
if buf.Len() > maxUploadSize {
|
|
slog.Warn("transcript exceeds 25MB, saving locally only", "path", outPath, "size_mb", buf.Len()/1024/1024)
|
|
}
|
|
|
|
if err := os.WriteFile(outPath, buf.Bytes(), 0644); err != nil {
|
|
return "", fmt.Errorf("write transcript: %w", err)
|
|
}
|
|
return outPath, nil
|
|
}
|
|
|
|
func (g *Generator) fetchAll(channelID string) ([]*discordgo.Message, error) {
|
|
var all []*discordgo.Message
|
|
var before string
|
|
for {
|
|
batch, err := g.Session.ChannelMessages(channelID, 100, before, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all = append(all, batch...)
|
|
if len(batch) < 100 {
|
|
break
|
|
}
|
|
before = batch[len(batch)-1].ID
|
|
}
|
|
for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 {
|
|
all[i], all[j] = all[j], all[i]
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
func renderMessage(m *discordgo.Message) Message {
|
|
// Webhook messages use WebhookID — author is the impersonated user
|
|
name := m.Author.Username
|
|
avatar := m.Author.AvatarURL("64")
|
|
initial := "?"
|
|
if len(name) > 0 {
|
|
initial = strings.ToUpper(string([]rune(name)[0]))
|
|
}
|
|
|
|
content := template.HTMLEscapeString(m.Content)
|
|
contentHTML := template.HTML(strings.ReplaceAll(content, "\n", "<br>"))
|
|
|
|
ts := ""
|
|
if !m.Timestamp.IsZero() {
|
|
ts = m.Timestamp.UTC().Format("02/01/2006 15:04:05")
|
|
}
|
|
|
|
var attachments []Attachment
|
|
for _, a := range m.Attachments {
|
|
attachments = append(attachments, Attachment{
|
|
Name: template.HTMLEscapeString(a.Filename),
|
|
URL: a.URL,
|
|
})
|
|
}
|
|
|
|
var embeds []RenderedEmbed
|
|
for _, e := range m.Embeds {
|
|
embeds = append(embeds, renderEmbed(e))
|
|
}
|
|
|
|
return Message{
|
|
AuthorName: template.HTMLEscapeString(name),
|
|
AuthorAvatar: avatar,
|
|
AuthorInitial: initial,
|
|
Content: contentHTML,
|
|
Timestamp: ts,
|
|
Attachments: attachments,
|
|
Embeds: embeds,
|
|
}
|
|
}
|
|
|
|
func renderEmbed(e *discordgo.MessageEmbed) RenderedEmbed {
|
|
re := RenderedEmbed{}
|
|
|
|
// Color
|
|
if e.Color != 0 {
|
|
re.Color = template.CSS(fmt.Sprintf("#%06x", e.Color))
|
|
} else {
|
|
re.Color = "#4f545c"
|
|
}
|
|
|
|
// Author
|
|
if e.Author != nil {
|
|
re.AuthorName = template.HTMLEscapeString(e.Author.Name)
|
|
re.AuthorIcon = e.Author.IconURL
|
|
re.AuthorURL = e.Author.URL
|
|
}
|
|
|
|
// Title
|
|
if e.Title != "" {
|
|
re.Title = template.HTMLEscapeString(e.Title)
|
|
re.TitleURL = e.URL
|
|
}
|
|
|
|
// Description
|
|
if e.Description != "" {
|
|
escaped := template.HTMLEscapeString(e.Description)
|
|
re.Description = template.HTML(strings.ReplaceAll(escaped, "\n", "<br>"))
|
|
}
|
|
|
|
// Fields
|
|
for _, f := range e.Fields {
|
|
val := template.HTMLEscapeString(f.Value)
|
|
re.Fields = append(re.Fields, EmbedField{
|
|
Name: template.HTMLEscapeString(f.Name),
|
|
Value: template.HTML(strings.ReplaceAll(val, "\n", "<br>")),
|
|
Inline: f.Inline,
|
|
})
|
|
}
|
|
|
|
// Image
|
|
if e.Image != nil {
|
|
re.ImageURL = e.Image.URL
|
|
}
|
|
|
|
// Thumbnail
|
|
if e.Thumbnail != nil {
|
|
re.ThumbURL = e.Thumbnail.URL
|
|
}
|
|
|
|
// Footer
|
|
if e.Footer != nil {
|
|
re.FooterText = template.HTMLEscapeString(e.Footer.Text)
|
|
re.FooterIcon = e.Footer.IconURL
|
|
}
|
|
|
|
// Timestamp
|
|
if e.Timestamp != "" {
|
|
if t, err := time.Parse(time.RFC3339, e.Timestamp); err == nil {
|
|
re.Timestamp = t.UTC().Format("02/01/2006 15:04")
|
|
}
|
|
}
|
|
|
|
// Detect empty embed (nothing visible)
|
|
re.Empty = re.AuthorName == "" && re.Title == "" &&
|
|
re.Description == "" && len(re.Fields) == 0 &&
|
|
re.ImageURL == "" && re.FooterText == ""
|
|
|
|
return re
|
|
}
|