mirror Discord replies as replies on X and bluesky
Replying to a message on the watched channel now posts the reply as a reply to the posts that message produced, instead of as a standalone post. Discord tells us the parent through MessageReference, and the store maps it back to the Ref each output returned; that Ref rides along on the new Post.ReplyTo. X takes it as in_reply_to_tweet_id. Bluesky needs the thread root as well as the parent, so a Ref now carries the root it was posted under and a post that is not itself a reply acts as its own root. Replying to something we never distributed, an error notice from the bot for instance, still posts normally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,28 @@ func (bo *blueskyoutput) Write(post Post) (Ref, error) {
|
||||
Langs: []string{"ja"},
|
||||
}
|
||||
|
||||
root := Ref{}
|
||||
if post.ReplyTo != nil && post.ReplyTo.URI != "" {
|
||||
parent := &atproto.RepoStrongRef{
|
||||
Uri: post.ReplyTo.URI,
|
||||
Cid: post.ReplyTo.CID,
|
||||
}
|
||||
// A post that is itself a reply carries the thread root; one that is
|
||||
// not is the root of its own thread.
|
||||
rootref := &atproto.RepoStrongRef{
|
||||
Uri: post.ReplyTo.RootURI,
|
||||
Cid: post.ReplyTo.RootCID,
|
||||
}
|
||||
if rootref.Uri == "" {
|
||||
rootref = parent
|
||||
}
|
||||
feedpost.Reply = &bsky.FeedPost_ReplyRef{
|
||||
Parent: parent,
|
||||
Root: rootref,
|
||||
}
|
||||
root = Ref{RootURI: rootref.Uri, RootCID: rootref.Cid}
|
||||
}
|
||||
|
||||
if len(post.Images) > 0 {
|
||||
embedimages := make([]*bsky.EmbedImages_Image, 0, len(post.Images))
|
||||
for _, img := range post.Images {
|
||||
@@ -96,7 +118,12 @@ func (bo *blueskyoutput) Write(post Post) (Ref, error) {
|
||||
return Ref{}, recerr
|
||||
}
|
||||
|
||||
return Ref{URI: record.Uri, CID: record.Cid}, nil
|
||||
return Ref{
|
||||
URI: record.Uri,
|
||||
CID: record.Cid,
|
||||
RootURI: root.RootURI,
|
||||
RootCID: root.RootCID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bo *blueskyoutput) Delete(ref Ref) error {
|
||||
|
||||
@@ -7,18 +7,28 @@ type Image struct {
|
||||
}
|
||||
|
||||
// Ref identifies a post an output has already published so it can later be
|
||||
// deleted. The fields are output specific; only the ones the publishing
|
||||
// output filled in are meaningful to it.
|
||||
// replied to or deleted. The fields are output specific; only the ones the
|
||||
// publishing output filled in are meaningful to it.
|
||||
type Ref struct {
|
||||
ID string `json:"id,omitempty"` // twitter: tweet ID
|
||||
URI string `json:"uri,omitempty"` // bluesky: at:// URI of the record
|
||||
CID string `json:"cid,omitempty"` // bluesky: record CID
|
||||
ID string `json:"id,omitempty"` // twitter: tweet ID
|
||||
URI string `json:"uri,omitempty"` // bluesky: at:// URI of the record
|
||||
CID string `json:"cid,omitempty"` // bluesky: record CID
|
||||
RootURI string `json:"rooturi,omitempty"`
|
||||
RootCID string `json:"rootcid,omitempty"`
|
||||
}
|
||||
|
||||
// IsZero reports whether the ref points at nothing.
|
||||
func (r Ref) IsZero() bool {
|
||||
return r == Ref{}
|
||||
}
|
||||
|
||||
// Post is a single message to distribute.
|
||||
type Post struct {
|
||||
Text string
|
||||
Images []Image
|
||||
// ReplyTo is the Ref this same output returned for the post being
|
||||
// replied to, or nil for a top level post.
|
||||
ReplyTo *Ref
|
||||
}
|
||||
|
||||
type OutputInterface interface {
|
||||
|
||||
@@ -12,6 +12,9 @@ func StdOutput() *stdoutput {
|
||||
}
|
||||
|
||||
func (so *stdoutput) Write(post Post) (Ref, error) {
|
||||
if post.ReplyTo != nil {
|
||||
fmt.Printf("[reply to: %s]\n", post.ReplyTo.ID)
|
||||
}
|
||||
fmt.Println(post.Text)
|
||||
for _, img := range post.Images {
|
||||
fmt.Printf("[image: %s (%s, %d bytes)]\n", img.Filename, img.ContentType, len(img.Data))
|
||||
|
||||
@@ -63,6 +63,12 @@ func (to *twitteroutput) Write(post Post) (Ref, error) {
|
||||
MediaIDs: mediaIDs,
|
||||
}
|
||||
}
|
||||
if post.ReplyTo != nil && post.ReplyTo.ID != "" {
|
||||
p.Reply = &types.CreateInputReply{
|
||||
InReplyToTweetID: post.ReplyTo.ID,
|
||||
}
|
||||
}
|
||||
|
||||
out, err := managetweet.Create(context.Background(), to.client, p)
|
||||
if err != nil {
|
||||
return Ref{}, err
|
||||
|
||||
Reference in New Issue
Block a user