Files
plumber/internal/discord/format.go
T
codegirl007 f96df3222d
CI / test (pull_request) Successful in 6m29s
Add video picker and Discord video attachments.
The picker accepts one MP4 or WebM, the thread lightbox zooms photos, and Discord gets the video as a file on a follow-up message so photo embeds still render.
2026-08-31 02:00:56 -07:00

174 lines
3.6 KiB
Go

package discord
import (
"bytes"
"io"
"strings"
"plumber/internal/events"
)
const (
embedTitleLimit = 256
embedDescriptionLimit = 4096
threadNameLimit = 100
embedColor = 0xe96a26
)
// Attachment is a local file Discord should upload with the message.
type Attachment struct {
Name string
ContentType string
Kind string
Reader io.Reader
}
// Message is a Discord-ready snapshot of a site post event.
type Message struct {
Title string
URL string
Description string
City string
Author string
ImageURLs []string
VideoURLs []string
Files []Attachment
ThreadName string
}
func formatMessage(ev events.PostEvent) Message {
return formatCreated(events.PostCreated{PostEvent: ev})
}
func formatCreated(e events.PostCreated) Message {
title := strings.TrimSpace(e.Title)
if title == "" {
title = "Reply"
}
author := strings.TrimSpace(e.AuthorName)
if author == "" {
author = "Someone"
}
msg := Message{
Title: truncateRunes(title, embedTitleLimit),
URL: strings.TrimSpace(e.Permalink),
Description: truncateRunes(strings.TrimSpace(e.Body), embedDescriptionLimit),
City: strings.TrimSpace(e.City),
Author: author,
ThreadName: threadName(author, e.Title),
Files: filesFromMedia(e.Media),
}
for _, img := range e.Images {
url := strings.TrimSpace(img.URL)
if url == "" {
continue
}
if img.Kind == "video" {
msg.VideoURLs = append(msg.VideoURLs, url)
continue
}
msg.ImageURLs = append(msg.ImageURLs, url)
}
return msg
}
func filesFromMedia(media []events.Media) []Attachment {
if len(media) == 0 {
return nil
}
files := make([]Attachment, 0, len(media))
for _, m := range media {
var r io.Reader
switch {
case len(m.Bytes) > 0:
r = bytes.NewReader(m.Bytes)
case m.Body != nil:
r = m.Body
default:
continue
}
name := strings.TrimSpace(m.Name)
if name == "" {
name = "upload"
}
files = append(files, Attachment{
Name: name,
ContentType: m.ContentType,
Kind: m.Kind,
Reader: r,
})
}
return files
}
func threadName(author, title string) string {
author = strings.TrimSpace(author)
if author == "" {
author = "Someone"
}
title = strings.TrimSpace(title)
if title == "" {
title = "Question"
}
return truncateRunes(author+" asks: "+title, threadNameLimit)
}
func truncateRunes(s string, max int) string {
if max <= 0 {
return ""
}
runes := []rune(s)
if len(runes) <= max {
return s
}
return string(runes[:max])
}
func isRoot(ev events.PostEvent) bool {
return strings.TrimSpace(ev.ParentID) == ""
}
func messageContent(msg Message) string {
var parts []string
if body := strings.TrimSpace(msg.Description); body != "" {
parts = append(parts, body)
}
var meta []string
if msg.City != "" {
meta = append(meta, msg.City)
}
if msg.Author != "" {
meta = append(meta, msg.Author)
}
if len(meta) > 0 {
parts = append(parts, strings.Join(meta, " · "))
}
if u := publicURL(msg.URL); u != "" {
parts = append(parts, u)
}
if len(msg.Files) == 0 {
for _, mediaURL := range msg.ImageURLs {
if u := publicURL(mediaURL); u != "" {
parts = append(parts, u)
}
}
for _, mediaURL := range msg.VideoURLs {
if u := publicURL(mediaURL); u != "" {
parts = append(parts, u)
}
}
}
return truncateRunes(strings.Join(parts, "\n"), 2000)
}
func publicURL(raw string) string {
raw = strings.TrimSpace(raw)
if !strings.HasPrefix(raw, "https://") {
return ""
}
if strings.Contains(raw, "localhost") || strings.Contains(raw, "127.0.0.1") {
return ""
}
return raw
}