Files
tweetdistributor/discord/discordread.go
sirrow 1c2ab29404 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>
2026-07-26 12:00:22 +09:00

93 lines
1.9 KiB
Go

package discord
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
// EventKind tells apart the things that can happen to a watched message.
type EventKind int
const (
MessageCreated EventKind = iota
MessageDeleted
)
// Event is a change on the watched channel that the distributor has to mirror.
type Event struct {
Kind EventKind
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 {
Token string
ChannelID string
dgsession *discordgo.Session
}
func Discord(token string, channelId string) *Client {
return &Client{
Token: token,
ChannelID: channelId,
}
}
func (d *Client) BeginRead(eventchannel chan<- Event) {
d.read(eventchannel)
}
func (d *Client) read(eventchannel chan<- Event) {
dgsession, err := discordgo.New("Bot " + d.Token)
if err != nil {
return
}
dgsession.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.ChannelID != d.ChannelID || m.Author.ID == s.State.User.ID {
return
}
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
// thing to filter on; messages we never distributed are dropped later.
dgsession.AddHandler(func(s *discordgo.Session, m *discordgo.MessageDelete) {
if m.ChannelID != d.ChannelID {
return
}
eventchannel <- Event{
Kind: MessageDeleted,
MessageID: m.ID,
}
})
d.dgsession = dgsession
errOpen := dgsession.Open()
if errOpen != nil {
fmt.Println(errOpen)
}
}
func Read() string {
return "discord"
}
func (d *Client) Write(str string) {
d.dgsession.ChannelMessageSend(d.ChannelID, str)
}