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:
@@ -20,6 +20,9 @@ type Event struct {
|
||||
MessageID string
|
||||
Content string
|
||||
Attachments []*discordgo.MessageAttachment
|
||||
// ReplyToID is the ID of the message this one replies to, empty when the
|
||||
// message is not a reply.
|
||||
ReplyToID string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -49,12 +52,16 @@ func (d *Client) read(eventchannel chan<- Event) {
|
||||
if m.ChannelID != d.ChannelID || m.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
eventchannel <- Event{
|
||||
event := Event{
|
||||
Kind: MessageCreated,
|
||||
MessageID: m.ID,
|
||||
Content: m.Content,
|
||||
Attachments: m.Attachments,
|
||||
}
|
||||
if m.MessageReference != nil {
|
||||
event.ReplyToID = m.MessageReference.MessageID
|
||||
}
|
||||
eventchannel <- event
|
||||
})
|
||||
|
||||
// Deleted messages arrive without an author, so the channel is the only
|
||||
|
||||
18
main.go
18
main.go
@@ -114,7 +114,8 @@ func (dist *distributor) reportf(format string, args ...any) {
|
||||
dist.d.Write(errstr)
|
||||
}
|
||||
|
||||
// created posts a new Discord message to every output.
|
||||
// created posts a new Discord message to every output, as a reply when the
|
||||
// Discord message itself was a reply to something we already distributed.
|
||||
func (dist *distributor) created(event discord.Event) {
|
||||
if length := utf8.RuneCountInString(event.Content); length > maxTweetLength {
|
||||
dist.reportf("Error: message is %d characters, exceeding the %d character limit; not posted", length, maxTweetLength)
|
||||
@@ -131,12 +132,23 @@ func (dist *distributor) created(event discord.Event) {
|
||||
return
|
||||
}
|
||||
|
||||
// A reply to a message we never distributed becomes a top level post.
|
||||
var parents store.Refs
|
||||
if event.ReplyToID != "" {
|
||||
parents, _ = dist.store.Get(event.ReplyToID)
|
||||
}
|
||||
|
||||
refs := store.Refs{}
|
||||
for _, out := range dist.outputs {
|
||||
ref, err := out.Write(output.Post{
|
||||
post := output.Post{
|
||||
Text: event.Content,
|
||||
Images: images,
|
||||
})
|
||||
}
|
||||
if parent, ok := parents[out.GetName()]; ok && !parent.IsZero() {
|
||||
post.ReplyTo = &parent
|
||||
}
|
||||
|
||||
ref, err := out.Write(post)
|
||||
if err != nil {
|
||||
dist.reportf("%s Error: %s", out.GetName(), err)
|
||||
continue
|
||||
|
||||
@@ -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
|
||||
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