Preserve inbound reply IDs across channels

This commit is contained in:
Dmitrii Balabanov 2026-03-23 10:53:54 +02:00
parent bd3017dd8e
commit 7f91fa90f2
6 changed files with 128 additions and 0 deletions

View file

@ -478,6 +478,9 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
"channel_id": m.ChannelID, "channel_id": m.ChannelID,
"is_dm": fmt.Sprintf("%t", m.GuildID == ""), "is_dm": fmt.Sprintf("%t", m.GuildID == ""),
} }
if m.MessageReference != nil && m.MessageReference.MessageID != "" {
metadata["reply_to_message_id"] = m.MessageReference.MessageID
}
c.HandleMessage(c.ctx, peer, m.ID, senderID, m.ChannelID, content, mediaPaths, metadata, sender) c.HandleMessage(c.ctx, peer, m.ID, senderID, m.ChannelID, content, mediaPaths, metadata, sender)
} }

View file

@ -1,11 +1,15 @@
package discord package discord
import ( import (
"context"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
) )
func TestApplyDiscordProxy_CustomProxy(t *testing.T) { func TestApplyDiscordProxy_CustomProxy(t *testing.T) {
@ -89,3 +93,43 @@ func TestApplyDiscordProxy_InvalidProxyURL(t *testing.T) {
t.Fatal("applyDiscordProxy() expected error for invalid proxy URL, got nil") t.Fatal("applyDiscordProxy() expected error for invalid proxy URL, got nil")
} }
} }
func TestHandleMessage_PreservesReplyToMessageID(t *testing.T) {
messageBus := bus.NewMessageBus()
ch := &DiscordChannel{
BaseChannel: channels.NewBaseChannel("discord", nil, messageBus, nil),
ctx: context.Background(),
}
session := &discordgo.Session{
State: &discordgo.State{
Ready: discordgo.Ready{
User: &discordgo.User{ID: "bot"},
},
},
}
ch.handleMessage(session, &discordgo.MessageCreate{
Message: &discordgo.Message{
ID: "msg-2",
ChannelID: "chan-1",
Content: "hello",
Author: &discordgo.User{
ID: "user-1",
Username: "alice",
},
MessageReference: &discordgo.MessageReference{
MessageID: "msg-1",
ChannelID: "chan-1",
},
},
})
inbound := <-messageBus.InboundChan()
if inbound.MessageID != "msg-2" {
t.Fatalf("expected MessageID msg-2, got %q", inbound.MessageID)
}
if inbound.ReplyToMessageID != "msg-1" {
t.Fatalf("expected ReplyToMessageID msg-1, got %q", inbound.ReplyToMessageID)
}
}

View file

@ -743,6 +743,7 @@ func (c *MatrixChannel) handleMessageEvent(ctx context.Context, evt *event.Event
"sender_raw": senderID, "sender_raw": senderID,
} }
if replyTo := msgEvt.GetRelatesTo().GetReplyTo(); replyTo != "" { if replyTo := msgEvt.GetRelatesTo().GetReplyTo(); replyTo != "" {
metadata["reply_to_message_id"] = replyTo.String()
metadata["reply_to_msg_id"] = replyTo.String() metadata["reply_to_msg_id"] = replyTo.String()
} }

View file

@ -14,6 +14,8 @@ import (
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/media" "github.com/sipeed/picoclaw/pkg/media"
) )
@ -386,3 +388,45 @@ func TestMessageContent(t *testing.T) {
t.Errorf("plain: expected no formatting, got format=%q formattedBody=%q", mc.Format, mc.FormattedBody) t.Errorf("plain: expected no formatting, got format=%q formattedBody=%q", mc.Format, mc.FormattedBody)
} }
} }
func TestHandleMessageEvent_PreservesReplyToMessageID(t *testing.T) {
messageBus := bus.NewMessageBus()
roomID := id.RoomID("!room:matrix.test")
ch := &MatrixChannel{
BaseChannel: channels.NewBaseChannel("matrix", nil, messageBus, nil),
client: &mautrix.Client{UserID: id.UserID("@bot:matrix.test")},
startTime: time.Unix(0, 0),
roomKindCache: newRoomKindCache(4, time.Minute),
}
ch.roomKindCache.set(roomID.String(), false, time.Now())
msgContent := &event.MessageEventContent{
MsgType: event.MsgText,
Body: "hello",
RelatesTo: &event.RelatesTo{
InReplyTo: &event.InReplyTo{EventID: id.EventID("$parent")},
},
}
ch.handleMessageEvent(context.Background(), &event.Event{
Sender: id.UserID("@alice:matrix.test"),
Type: event.EventMessage,
Timestamp: time.Now().UnixMilli(),
ID: id.EventID("$event"),
RoomID: roomID,
Content: event.Content{
Parsed: msgContent,
},
})
inbound := <-messageBus.InboundChan()
if inbound.MessageID != "$event" {
t.Fatalf("expected MessageID $event, got %q", inbound.MessageID)
}
if inbound.ReplyToMessageID != "$parent" {
t.Fatalf("expected ReplyToMessageID $parent, got %q", inbound.ReplyToMessageID)
}
if inbound.Metadata["reply_to_message_id"] != "$parent" {
t.Fatalf("expected metadata reply_to_message_id $parent, got %q", inbound.Metadata["reply_to_message_id"])
}
}

View file

@ -375,6 +375,9 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
"platform": "slack", "platform": "slack",
"team_id": c.teamID, "team_id": c.teamID,
} }
if threadTS != "" && threadTS != messageTS {
metadata["reply_to_message_id"] = threadTS
}
logger.DebugCF("slack", "Received message", map[string]any{ logger.DebugCF("slack", "Received message", map[string]any{
"sender_id": senderID, "sender_id": senderID,
@ -447,6 +450,9 @@ func (c *SlackChannel) handleAppMention(ev *slackevents.AppMentionEvent) {
"is_mention": "true", "is_mention": "true",
"team_id": c.teamID, "team_id": c.teamID,
} }
if threadTS != "" && threadTS != messageTS {
metadata["reply_to_message_id"] = threadTS
}
c.HandleMessage(c.ctx, mentionPeer, messageTS, senderID, chatID, content, nil, metadata, mentionSender) c.HandleMessage(c.ctx, mentionPeer, messageTS, senderID, chatID, content, nil, metadata, mentionSender)
} }

View file

@ -1,9 +1,13 @@
package slack package slack
import ( import (
"context"
"testing" "testing"
"github.com/slack-go/slack/slackevents"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
) )
@ -168,3 +172,29 @@ func TestSlackChannelIsAllowed(t *testing.T) {
} }
}) })
} }
func TestHandleMessageEvent_PreservesReplyToMessageIDFromThreadTS(t *testing.T) {
messageBus := bus.NewMessageBus()
ch := &SlackChannel{
BaseChannel: channels.NewBaseChannel("slack", nil, messageBus, nil),
ctx: context.Background(),
teamID: "T1",
}
ch.handleMessageEvent(&slackevents.MessageEvent{
Type: "message",
User: "U123",
Text: "hello",
ThreadTimeStamp: "1710000000.000100",
TimeStamp: "1710000000.000200",
Channel: "C123",
})
inbound := <-messageBus.InboundChan()
if inbound.MessageID != "1710000000.000200" {
t.Fatalf("expected MessageID to be message ts, got %q", inbound.MessageID)
}
if inbound.ReplyToMessageID != "1710000000.000100" {
t.Fatalf("expected ReplyToMessageID thread root ts, got %q", inbound.ReplyToMessageID)
}
}