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:
@@ -3,7 +3,9 @@ package output
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bluesky-social/indigo/api/atproto"
|
||||
@@ -13,6 +15,8 @@ import (
|
||||
"github.com/bluesky-social/indigo/xrpc"
|
||||
)
|
||||
|
||||
const blueskyCollection = "app.bsky.feed.post"
|
||||
|
||||
type blueskyoutput struct {
|
||||
identifier string
|
||||
password string
|
||||
@@ -27,7 +31,8 @@ func BlueskyOutput(identifier string, password string) *blueskyoutput {
|
||||
return blueskyoutput
|
||||
}
|
||||
|
||||
func (bo *blueskyoutput) Write(str string, images []Image) error {
|
||||
// session logs in and returns a client authenticated as the configured account.
|
||||
func (bo *blueskyoutput) session() (*xrpc.Client, error) {
|
||||
cli := &xrpc.Client{
|
||||
Host: "https://bsky.social",
|
||||
}
|
||||
@@ -39,7 +44,7 @@ func (bo *blueskyoutput) Write(str string, images []Image) error {
|
||||
|
||||
output, err := atproto.ServerCreateSession(context.TODO(), cli, input)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
cli.Auth = &xrpc.AuthInfo{
|
||||
AccessJwt: output.AccessJwt,
|
||||
@@ -47,26 +52,34 @@ func (bo *blueskyoutput) Write(str string, images []Image) error {
|
||||
Handle: output.Handle,
|
||||
Did: output.Did,
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
post := &bsky.FeedPost{
|
||||
Text: str,
|
||||
func (bo *blueskyoutput) Write(post Post) (Ref, error) {
|
||||
cli, err := bo.session()
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
}
|
||||
|
||||
feedpost := &bsky.FeedPost{
|
||||
Text: post.Text,
|
||||
CreatedAt: time.Now().Format(util.ISO8601),
|
||||
Langs: []string{"ja"},
|
||||
}
|
||||
|
||||
if len(images) > 0 {
|
||||
embedimages := make([]*bsky.EmbedImages_Image, 0, len(images))
|
||||
for _, img := range images {
|
||||
if len(post.Images) > 0 {
|
||||
embedimages := make([]*bsky.EmbedImages_Image, 0, len(post.Images))
|
||||
for _, img := range post.Images {
|
||||
blob, err := atproto.RepoUploadBlob(context.TODO(), cli, bytes.NewReader(img.Data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("uploading %s: %w", img.Filename, err)
|
||||
return Ref{}, fmt.Errorf("uploading %s: %w", img.Filename, err)
|
||||
}
|
||||
embedimages = append(embedimages, &bsky.EmbedImages_Image{
|
||||
Alt: img.Filename,
|
||||
Image: blob.Blob,
|
||||
})
|
||||
}
|
||||
post.Embed = &bsky.FeedPost_Embed{
|
||||
feedpost.Embed = &bsky.FeedPost_Embed{
|
||||
EmbedImages: &bsky.EmbedImages{
|
||||
Images: embedimages,
|
||||
},
|
||||
@@ -74,13 +87,47 @@ func (bo *blueskyoutput) Write(str string, images []Image) error {
|
||||
}
|
||||
|
||||
Recordinput := &atproto.RepoCreateRecord_Input{
|
||||
Collection: "app.bsky.feed.post",
|
||||
Collection: blueskyCollection,
|
||||
Repo: cli.Auth.Did, // "matope.bsky.social" のDID
|
||||
Record: &lexutil.LexiconTypeDecoder{Val: post},
|
||||
Record: &lexutil.LexiconTypeDecoder{Val: feedpost},
|
||||
}
|
||||
record, recerr := atproto.RepoCreateRecord(context.TODO(), cli, Recordinput)
|
||||
if recerr != nil {
|
||||
return Ref{}, recerr
|
||||
}
|
||||
_, recerr := atproto.RepoCreateRecord(context.TODO(), cli, Recordinput)
|
||||
|
||||
return recerr
|
||||
return Ref{URI: record.Uri, CID: record.Cid}, nil
|
||||
}
|
||||
|
||||
func (bo *blueskyoutput) Delete(ref Ref) error {
|
||||
rkey, err := recordKey(ref.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli, err := bo.session()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = atproto.RepoDeleteRecord(context.TODO(), cli, &atproto.RepoDeleteRecord_Input{
|
||||
Collection: blueskyCollection,
|
||||
Repo: cli.Auth.Did,
|
||||
Rkey: rkey,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// recordKey pulls the rkey out of an at://did/collection/rkey URI.
|
||||
func recordKey(uri string) (string, error) {
|
||||
if uri == "" {
|
||||
return "", errors.New("no record URI to delete")
|
||||
}
|
||||
idx := strings.LastIndex(uri, "/")
|
||||
if idx < 0 || idx == len(uri)-1 {
|
||||
return "", fmt.Errorf("malformed record URI %q", uri)
|
||||
}
|
||||
return uri[idx+1:], nil
|
||||
}
|
||||
|
||||
func (bo *blueskyoutput) GetName() string {
|
||||
|
||||
Reference in New Issue
Block a user