Files
Cyrene_v2/plugins/channels/kook/identity.go
T
Aska db8465f68b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(kook): 丢弃自我回声消息,防 bot 把自己的回复当入站用户消息
KOOK 网关会把 bot 自己发的消息又推回给 bot(author_id=bot uid),导致 bot 把自己的
回复当成入站用户消息处理(循环/浪费token)。toUnifiedMessage 计算 senderID 后调用
isSelf(senderID) 比对 a.botID(user/me 解析出的 bot 账号 id),命中即返回 ok=false 丢弃。
- identity.go: 新增 isSelf(并发安全,botMu)
- message.go: self-echo 丢弃 + debug 日志
- message_test.go: TestToUnified_DropsSelfEcho
验收: go build/vet/test ./... 全绿
2026-09-07 18:38:15 +08:00

103 lines
3.8 KiB
Go

package kook
// This file adds the "token 即用、其余自动获取" bot-identity lookup. After the
// adapter holds a token, Connect calls GET /api/v3/user/me to resolve the bot
// account (id/username/nickname) so the admin channel list can show who the bot
// is without the user hand-configuring any non-token field. The lookup is
// best-effort: a bad token or a transient network failure logs a warning and
// leaves the bot identity empty, but the adapter still establishes its session
// and routes messages normally. The identity is surfaced through Config() under
// the non-sensitive bot_id/bot_username/bot_nickname keys, so it reaches the
// admin read path without ever persisting or logging the token.
import (
"context"
"encoding/json"
"fmt"
)
// userMePath is the KOOK REST endpoint that returns the bot account for a bearer
// token.
const userMePath = "/user/me"
// botIdentity is the "data" payload of GET /api/v3/user/me. Only the three
// display-relevant fields are captured; the rest of the payload is dropped.
type botIdentity struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
}
// fetchBotIdentity calls GET /api/v3/user/me with the bearer token and decodes
// the bot account. An empty id is treated as a bad response, so a caller can
// distinguish "no account" from "no identity yet".
func (a *Adapter) fetchBotIdentity(ctx context.Context) (botIdentity, error) {
data, err := a.http.GetJSON(ctx, userMePath, nil)
if err != nil {
return botIdentity{}, fmt.Errorf("kook: fetch bot identity: %w", err)
}
var bi botIdentity
if err := json.Unmarshal(data, &bi); err != nil {
return botIdentity{}, fmt.Errorf("kook: decode bot identity: %w", err)
}
if bi.ID == "" {
return botIdentity{}, fmt.Errorf("kook: bot identity empty: %w", ErrBadResponse)
}
return bi, nil
}
// refreshBotIdentity resolves the bot account (best-effort) and stores it on the
// adapter. A failure is logged and ignored so a bad token or network hiccup never
// aborts the connect; the identity is simply left empty. Only the bot's public
// handle/display name is logged — never the token or the user id.
func (a *Adapter) refreshBotIdentity(ctx context.Context) {
bi, err := a.fetchBotIdentity(ctx)
if err != nil {
a.log.Warn("bot identity lookup failed", "err", err)
return
}
a.setBotIdentity(bi)
if bi.Nickname != "" {
a.log.Info("connected as bot", "nickname", bi.Nickname)
} else if bi.Username != "" {
a.log.Info("connected as bot", "username", bi.Username)
}
}
// setBotIdentity stores the resolved bot account under a dedicated mutex.
func (a *Adapter) setBotIdentity(bi botIdentity) {
a.botMu.Lock()
defer a.botMu.Unlock()
a.botID = bi.ID
a.botUsername = bi.Username
a.botNickname = bi.Nickname
}
// botIdentitySnapshot returns the last resolved bot account (empty until a
// successful Connect + user/me lookup).
func (a *Adapter) botIdentitySnapshot() (id, username, nickname string) {
a.botMu.RLock()
defer a.botMu.RUnlock()
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.
func (a *Adapter) BotIdentity() (id, username, nickname string) {
return a.botIdentitySnapshot()
}