save first version
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user