mirror Discord deletions to X and bluesky
Deleting a message on the watched channel now deletes the posts it
produced. Making that possible reshapes how a message reaches an output:
- OutputInterface takes an output.Post and returns an output.Ref, the
identifier the platform gave the new post (tweet ID for X, URI and
CID for bluesky). It also gained Delete(Ref).
- The discord package no longer forwards raw MessageCreate values but
a discord.Event carrying the kind of change, so the MessageDelete
handler can use the same channel. Deleted messages arrive without an
author, so only the channel ID can be filtered on.
- The new store package remembers Discord message ID -> per output Ref.
Set POST_STORE to a path to keep that mapping across restarts;
without it the mapping is memory only and a restart makes older
messages undeletable. It holds the most recent 1000 messages.
main.go grows a distributor type to hold the outputs and the store
rather than threading them through the event loop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
65
main_test.go
Normal file
65
main_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"tweetdistributor/output"
|
||||
)
|
||||
|
||||
// noiseJPEG returns a JPEG image of random noise, which compresses poorly
|
||||
// and reliably exceeds maxImageBytes at large dimensions.
|
||||
func noiseJPEG(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.RGBA{uint8(rng.Intn(256)), uint8(rng.Intn(256)), uint8(rng.Intn(256)), 255})
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 95}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestShrinkImageLarge(t *testing.T) {
|
||||
data := noiseJPEG(t, 3000, 3000)
|
||||
if len(data) <= maxImageBytes {
|
||||
t.Fatalf("test image should exceed maxImageBytes, got %d", len(data))
|
||||
}
|
||||
|
||||
shrunk, err := shrinkImage(output.Image{Data: data, ContentType: "image/jpeg", Filename: "DSC_7277.jpg"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(shrunk.Data) > maxImageBytes {
|
||||
t.Errorf("shrunk image is %d bytes, want <= %d", len(shrunk.Data), maxImageBytes)
|
||||
}
|
||||
if shrunk.ContentType != "image/jpeg" {
|
||||
t.Errorf("ContentType = %q, want image/jpeg", shrunk.ContentType)
|
||||
}
|
||||
if shrunk.Filename != "DSC_7277.jpg" {
|
||||
t.Errorf("Filename = %q, want DSC_7277.jpg", shrunk.Filename)
|
||||
}
|
||||
if _, _, err := image.Decode(bytes.NewReader(shrunk.Data)); err != nil {
|
||||
t.Errorf("shrunk image is not decodable: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShrinkImageSmallPassthrough(t *testing.T) {
|
||||
data := noiseJPEG(t, 100, 100)
|
||||
img := output.Image{Data: data, ContentType: "image/png", Filename: "small.png"}
|
||||
shrunk, err := shrinkImage(img)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(shrunk.Data, img.Data) || shrunk.ContentType != img.ContentType || shrunk.Filename != img.Filename {
|
||||
t.Error("small image should pass through unchanged")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user