diff --git a/plugins/channels/kook/identity.go b/plugins/channels/kook/identity.go index 6093caf..98ce34f 100644 --- a/plugins/channels/kook/identity.go +++ b/plugins/channels/kook/identity.go @@ -81,6 +81,19 @@ func (a *Adapter) botIdentitySnapshot() (id, username, nickname string) { return a.botID, a.botUsername, a.botNickname } +// isSelf reports whether senderID is the bot's own account id. KOOK delivers the +// bot's own messages back over the gateway, so an inbound event whose author_id equals +// the bot's user id is a self-echo and must be dropped — otherwise the bridge would +// process the bot's replies as inbound user messages and loop (or burn tokens). +func (a *Adapter) isSelf(senderID string) bool { + if senderID == "" { + return false + } + a.botMu.RLock() + defer a.botMu.RUnlock() + return a.botID != "" && senderID == a.botID +} + // BotIdentity returns the resolved bot account (id/username/nickname). The values // are non-sensitive (the bot's public handle and display name) so the admin panel // may show them. diff --git a/plugins/channels/kook/message.go b/plugins/channels/kook/message.go index b95229e..2a148b5 100644 --- a/plugins/channels/kook/message.go +++ b/plugins/channels/kook/message.go @@ -82,6 +82,12 @@ func (a *Adapter) toUnifiedMessage(data json.RawMessage, frame []byte) (adapter. _ = json.Unmarshal(ev.Extra, &ex) senderID, senderName := senderOf(&ev, &ex) + // Self-echo: KOOK pushes the bot's own channel messages back to it via the gateway. + // Drop them so the bridge never treats the bot's replies as inbound user messages. + if a.isSelf(senderID) { + a.log.Debug("self message dropped (echo)", "sender_id", senderID, "channel", ev.TargetID) + return adapter.UnifiedMessage{}, false + } ct, media := messageKind(ev.Type, content) group := channelIsGroup(ev.ChannelType) diff --git a/plugins/channels/kook/message_test.go b/plugins/channels/kook/message_test.go index 57be1c4..4f84881 100644 --- a/plugins/channels/kook/message_test.go +++ b/plugins/channels/kook/message_test.go @@ -7,6 +7,21 @@ import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) +func TestToUnified_DropsSelfEcho(t *testing.T) { + t.Parallel() + a := New(Config{BotToken: "x"}) + a.setBotIdentity(botIdentity{ID: "bot-1", Username: "Cyrene", Nickname: "Cyrene"}) + // KOOK pushes the bot's own channel message back to it; author_id == bot id. + data := json.RawMessage(`{ + "channel_type":"GROUP","type":1,"target_id":"chan-1","author_id":"bot-1", + "content":"咦,亲爱的这是想戳人家哪里呀♪","msg_id":"m-self","msg_timestamp":1700000000000,"nonce":"", + "extra":{"type":1,"guild_id":"g-1","mention":[],"author":{"id":"bot-1","username":"Cyrene","nickname":"Cyrene"}} + }`) + if _, ok := a.toUnifiedMessage(data, nil); ok { + t.Fatal("self-echo message should be dropped (ok=false)") + } +} + func TestToUnified_GroupText(t *testing.T) { t.Parallel() a := New(Config{BotToken: "x"})