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
+129 -3
View File
@@ -6,6 +6,7 @@ import (
"strings"
"sync"
"testing"
"time"
"plumber/internal/events"
"plumber/internal/store"
@@ -209,6 +210,125 @@ func TestOutboundUpdateWithoutLinkCreates(t *testing.T) {
}
}
func TestOutboundAttachesVideoFile(t *testing.T) {
t.Parallel()
bus := events.New()
defer bus.Close()
events.SubscribeRelease(bus)
links := newMemoryLinks()
api := &fakeAPI{}
bot := New("channel-1", links, api)
bot.bus = bus
released := make(chan struct{})
bot.Handle(context.Background(), events.PostCreated{
PostEvent: events.PostEvent{
PostID: "root-1",
RootID: "root-1",
Title: "Valve clip",
Body: "Watch the handle.",
AuthorName: "sam",
Permalink: "https://www.askaplumberfirst.com/questions/root-1#post-root-1",
Images: []events.Image{{URL: "https://cdn.example/clip.mp4", Kind: "video"}},
},
Media: []events.Media{{
Name: "clip.mp4",
ContentType: "video/mp4",
Kind: "video",
Bytes: []byte("fake-mp4"),
}},
Release: func() { close(released) },
})
if len(api.sends) != 3 ||
api.sends[1].Kind != "thread-msg" ||
api.sends[2].Kind != "thread-msg" {
t.Fatalf("sends = %+v", api.sends)
}
if len(api.sends[1].Msg.Files) != 0 {
t.Fatalf("text message files = %+v", api.sends[1].Msg.Files)
}
files := api.sends[2].Msg.Files
if len(files) != 1 || files[0].Name != "clip.mp4" || files[0].ContentType != "video/mp4" {
t.Fatalf("video files = %+v", files)
}
content := messageContent(api.sends[1].Msg)
if strings.Contains(content, "https://cdn.example/clip.mp4") {
t.Fatalf("content still has video URL: %q", content)
}
select {
case <-released:
case <-time.After(time.Second):
t.Fatal("PostedToDiscord did not release")
}
}
func TestOutboundPhotosThenVideo(t *testing.T) {
t.Parallel()
api := &fakeAPI{}
bot := New("channel-1", newMemoryLinks(), api)
bot.Handle(context.Background(), events.PostCreated{
PostEvent: events.PostEvent{
PostID: "root-1",
RootID: "root-1",
Title: "Valve clip",
Body: "Photo and video.",
AuthorName: "sam",
Images: []events.Image{
{URL: "https://cdn.example/a.jpg"},
{URL: "https://cdn.example/clip.mp4", Kind: "video"},
},
},
Media: []events.Media{
{Name: "still.jpg", ContentType: "image/jpeg", Kind: "image", Bytes: []byte("jpeg")},
{Name: "clip.mp4", ContentType: "video/mp4", Kind: "video", Bytes: []byte("mp4")},
},
})
if len(api.sends) != 3 ||
api.sends[1].Kind != "thread-msg" ||
api.sends[2].Kind != "thread-msg" {
t.Fatalf("sends = %+v", api.sends)
}
photos := api.sends[1].Msg.Files
if len(photos) != 0 {
t.Fatalf("text message should not attach files: %+v", photos)
}
if got := api.sends[1].Msg.ImageURLs; len(got) != 1 || got[0] != "https://cdn.example/a.jpg" {
t.Fatalf("text message images = %v", got)
}
if embeds := toEmbeds(api.sends[1].Msg); len(embeds) == 0 || embeds[0].Image == nil || embeds[0].Image.URL != "https://cdn.example/a.jpg" {
t.Fatalf("text embeds = %+v", toEmbeds(api.sends[1].Msg))
}
videos := api.sends[2].Msg.Files
if len(videos) != 1 || videos[0].Name != "clip.mp4" {
t.Fatalf("video files = %+v", videos)
}
}
func TestOutboundReleasesWhenAlreadyLinked(t *testing.T) {
t.Parallel()
links := newMemoryLinks()
if err := links.Upsert(context.Background(), store.DiscordLink{
PostID: "root-1",
MessageID: "d-root",
ThreadID: "thread-1",
}); err != nil {
t.Fatal(err)
}
released := false
bot := New("channel-1", links, &fakeAPI{})
bot.Handle(context.Background(), events.PostCreated{
PostEvent: events.PostEvent{PostID: "root-1", RootID: "root-1", Title: "Already posted"},
Release: func() { released = true },
})
if !released {
t.Fatal("skipped send did not release held upload")
}
}
func TestFormatMessage(t *testing.T) {
t.Parallel()
@@ -218,7 +338,10 @@ func TestFormatMessage(t *testing.T) {
City: "Oakland",
AuthorName: "sam",
Permalink: "https://example.com/q",
Images: []events.Image{{URL: "https://cdn.example/a.jpg", Description: "ignored"}},
Images: []events.Image{
{URL: "https://cdn.example/a.jpg", Description: "ignored"},
{URL: "https://cdn.example/clip.mp4", Kind: "video"},
},
})
if got.Title != "Leaky sink" ||
got.Description != "It drips." ||
@@ -226,7 +349,8 @@ func TestFormatMessage(t *testing.T) {
got.Author != "sam" ||
got.URL != "https://example.com/q" ||
got.ThreadName != "sam asks: Leaky sink" ||
len(got.ImageURLs) != 1 {
len(got.ImageURLs) != 1 || got.ImageURLs[0] != "https://cdn.example/a.jpg" ||
len(got.VideoURLs) != 1 || got.VideoURLs[0] != "https://cdn.example/clip.mp4" {
t.Fatalf("format = %+v", got)
}
@@ -237,7 +361,9 @@ func TestFormatMessage(t *testing.T) {
content := messageContent(got)
if strings.Contains(content, "Leaky sink") ||
!strings.Contains(content, "It drips.") ||
!strings.Contains(content, "Oakland") {
!strings.Contains(content, "Oakland") ||
!strings.Contains(content, "https://cdn.example/a.jpg") ||
!strings.Contains(content, "https://cdn.example/clip.mp4") {
t.Fatalf("content = %q", content)
}
if publicURL("/questions/x") != "" || publicURL("http://localhost:8080/q") != "" {