Files
bot_ticket_r6_elite/internal/transcript/generator.go
T
2026-05-02 02:55:53 +02:00

148 lines
3.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
}
type Attachment struct {
Name string
URL string
}
type Generator struct {
OutputDir string
Session *discordgo.Session
}
func NewGenerator(outputDir string, s *discordgo.Session) *Generator {
return &Generator{OutputDir: outputDir, Session: s}
}
// Generate fetches all messages from the ticket channel and writes the HTML transcript.
// Returns the output file path.
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
}
// Reverse to chronological order
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 {
name := m.Author.Username
avatar := m.Author.AvatarURL("64")
initial := "?"
if len(name) > 0 {
initial = strings.ToUpper(string([]rune(name)[0]))
}
// Escape all user content — XSS protection
content := template.HTMLEscapeString(m.Content)
// Preserve newlines as <br>
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,
})
}
return Message{
AuthorName: template.HTMLEscapeString(name),
AuthorAvatar: avatar,
AuthorInitial: initial,
Content: contentHTML,
Timestamp: ts,
Attachments: attachments,
}
}