Add video picker and Discord video attachments.
CI / test (pull_request) Successful in 6m29s

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.
This commit is contained in:
2026-08-31 02:00:56 -07:00
parent 84dea8ea5d
commit f96df3222d
22 changed files with 1018 additions and 144 deletions
+69 -7
View File
@@ -1,6 +1,8 @@
package discord
import (
"bytes"
"io"
"strings"
"plumber/internal/events"
@@ -13,6 +15,14 @@ const (
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
@@ -21,36 +31,76 @@ type Message struct {
City string
Author string
ImageURLs []string
VideoURLs []string
Files []Attachment
ThreadName string
}
func formatMessage(ev events.PostEvent) Message {
title := strings.TrimSpace(ev.Title)
return formatCreated(events.PostCreated{PostEvent: ev})
}
func formatCreated(e events.PostCreated) Message {
title := strings.TrimSpace(e.Title)
if title == "" {
title = "Reply"
}
author := strings.TrimSpace(ev.AuthorName)
author := strings.TrimSpace(e.AuthorName)
if author == "" {
author = "Someone"
}
msg := Message{
Title: truncateRunes(title, embedTitleLimit),
URL: strings.TrimSpace(ev.Permalink),
Description: truncateRunes(strings.TrimSpace(ev.Body), embedDescriptionLimit),
City: strings.TrimSpace(ev.City),
URL: strings.TrimSpace(e.Permalink),
Description: truncateRunes(strings.TrimSpace(e.Body), embedDescriptionLimit),
City: strings.TrimSpace(e.City),
Author: author,
ThreadName: threadName(author, ev.Title),
ThreadName: threadName(author, e.Title),
Files: filesFromMedia(e.Media),
}
for _, img := range ev.Images {
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 == "" {
@@ -96,6 +146,18 @@ func messageContent(msg Message) string {
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)
}