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() }