feat(channel): KOOK 渠道适配器 + 框架兼容增强(wsutil/httpclient)
- plugins/channels/kook: config/adapter/gateway/websocket/send/message/errors — 实现 PlatformAdapter 13方法 - WS 有状态网关: 取gateway地址→连WS→信令0/1/2/3/4/6 + 30s心跳/6s超时 + resume(拼接sn/session_id) + 指数退避重连 - 收事件: 信令0 → UnifiedMessage(word注 channel_type/type/target_id/extra.author→字段) - 发消息: /message/create(频道) + /direct-message/create(私聊,修正正确端点,非user-chat) + Bearer鉴权 - 注册 channels.go + CHANNELS_JSON 多账号 + channelbridge.configured 加 kook(bot_token非空才连) - 框架增强: internal/channel/adapter/wsutil(连接管理/心跳/重连/resume骨架,复用gorilla,KOOK用上) + httpclient(Bearer+envelope归一,KOOK用) - 全仓 go build/vet/test 绿, 零新依赖(复用餐gorilla), 未动telegram/qq/weixin/webhook契约 - §4.6 隔离/黑白名单/第三方边界未破坏 注: KOOK WS/WH 双模式待补(ReceiveMode), 实测需主人提供 CHANNEL_KOOK_TOKEN
This commit is contained in:
1 parent
eab6b63eb0
commit
764d592859
24 files changed
+2800
-2
No files matched your search
@@ -0,0 +1,202 @@
|
||||
package kook
|
||||
|
||||
// This file carries the KOOK protocol types and the conversion between a native
|
||||
// s=0 event payload and the normalized adapter.UnifiedMessage model. KOOK events
|
||||
// are delivered over the gateway WebSocket: channel_type distinguishes group
|
||||
// (GROUP/BROADCAST) from private (PERSON), type declares the content kind
|
||||
// (1 text / 2 image / 3 video / 4 file / 8 audio / 9 kmarkdown / 10 card /
|
||||
// 255 system), author_id==1 marks a system sender, and extra.author carries the
|
||||
// sender's user object (id/username/nickname).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
)
|
||||
|
||||
const platformName = "kook"
|
||||
|
||||
// KOOK message content kinds (the "type" field of an s=0 event).
|
||||
const (
|
||||
kindText = 1
|
||||
kindImage = 2
|
||||
kindVideo = 3
|
||||
kindFile = 4
|
||||
kindAudio = 8
|
||||
kindKMarkdown = 9
|
||||
kindCard = 10
|
||||
kindSystem = 255
|
||||
)
|
||||
|
||||
// KOOK channel types (the "channel_type" field of an s=0 event).
|
||||
const (
|
||||
channelGroup = "GROUP"
|
||||
channelBroadcast = "BROADCAST"
|
||||
)
|
||||
|
||||
// kookEvent is the "d" payload of an s=0 message event.
|
||||
type kookEvent struct {
|
||||
ChannelType string `json:"channel_type"`
|
||||
Type int `json:"type"`
|
||||
TargetID string `json:"target_id"`
|
||||
AuthorID string `json:"author_id"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
MsgID string `json:"msg_id"`
|
||||
MsgTimestamp int64 `json:"msg_timestamp"`
|
||||
Nonce string `json:"nonce"`
|
||||
Extra json.RawMessage `json:"extra"`
|
||||
}
|
||||
|
||||
// kookExtra carries the per-event auxiliary data (author, guild/channel names,
|
||||
// mentions). KOOK's field set varies by message type, so all fields are optional.
|
||||
type kookExtra struct {
|
||||
Type int `json:"type"`
|
||||
GuildID string `json:"guild_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
Mention []string `json:"mention"`
|
||||
Author *kookUser `json:"author"`
|
||||
}
|
||||
|
||||
// kookUser is the sender's user object carried by extra.author.
|
||||
type kookUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
// toUnifiedMessage converts a parsed s=0 event payload into a normalized
|
||||
// UnifiedMessage. It returns ok=false for events that carry no deliverable chat
|
||||
// message (system messages, non-chat notifications).
|
||||
func (a *Adapter) toUnifiedMessage(data json.RawMessage, frame []byte) (adapter.UnifiedMessage, bool) {
|
||||
var ev kookEvent
|
||||
if err := json.Unmarshal(data, &ev); err != nil {
|
||||
return adapter.UnifiedMessage{}, false
|
||||
}
|
||||
if ev.Type == kindSystem || ev.AuthorID == "1" {
|
||||
return adapter.UnifiedMessage{}, false
|
||||
}
|
||||
|
||||
content := asString(ev.Content)
|
||||
var ex kookExtra
|
||||
_ = json.Unmarshal(ev.Extra, &ex)
|
||||
|
||||
senderID, senderName := senderOf(&ev, &ex)
|
||||
ct, media := messageKind(ev.Type, content)
|
||||
group := channelIsGroup(ev.ChannelType)
|
||||
|
||||
msg := adapter.UnifiedMessage{
|
||||
Platform: platformName,
|
||||
ChannelType: group,
|
||||
ChannelID: ev.TargetID,
|
||||
SenderID: senderID,
|
||||
SenderName: senderName,
|
||||
Content: content,
|
||||
ContentType: ct,
|
||||
Media: media,
|
||||
Mentions: mentionsFrom(ex.Mention),
|
||||
MessageID: ev.MsgID,
|
||||
Timestamp: time.UnixMilli(ev.MsgTimestamp),
|
||||
RawData: frame,
|
||||
Sender: adapter.Actor{
|
||||
Role: adapter.RoleUnknown,
|
||||
UserID: senderID,
|
||||
DisplayName: senderName,
|
||||
GroupID: "",
|
||||
GroupName: ex.ChannelName,
|
||||
IsGroup: bool(group == adapter.ChannelGroup),
|
||||
},
|
||||
}
|
||||
|
||||
if group == adapter.ChannelGroup {
|
||||
gid := ex.GuildID
|
||||
if gid == "" {
|
||||
gid = ev.TargetID
|
||||
}
|
||||
msg.GroupID = gid
|
||||
msg.GroupName = ex.ChannelName
|
||||
msg.Sender.GroupID = gid
|
||||
msg.Sender.GroupName = ex.ChannelName
|
||||
a.rememberChannel(msg)
|
||||
}
|
||||
|
||||
// Adjust ChannelID: for private messages target_id is the peer user id, which
|
||||
// is already set above; for group messages target_id is the channel id.
|
||||
return msg, true
|
||||
}
|
||||
|
||||
// senderOf resolves the sender id and display name from the event fields,
|
||||
// preferring the extra.author nickname (design/16 §4.6: third parties must be
|
||||
// shown by nickname) with fallbacks to username and the author_id.
|
||||
func senderOf(ev *kookEvent, ex *kookExtra) (id, name string) {
|
||||
id = ev.AuthorID
|
||||
name = ev.AuthorID
|
||||
if ex.Author == nil {
|
||||
return id, name
|
||||
}
|
||||
if ex.Author.ID != "" {
|
||||
id = ex.Author.ID
|
||||
}
|
||||
switch {
|
||||
case ex.Author.Nickname != "":
|
||||
name = ex.Author.Nickname
|
||||
case ex.Author.Username != "":
|
||||
name = ex.Author.Username
|
||||
default:
|
||||
name = id
|
||||
}
|
||||
return id, name
|
||||
}
|
||||
|
||||
// channelIsGroup maps a KOOK channel_type onto the adapter's ChannelType.
|
||||
func channelIsGroup(t string) adapter.ChannelType {
|
||||
if t == channelGroup || t == channelBroadcast {
|
||||
return adapter.ChannelGroup
|
||||
}
|
||||
return adapter.ChannelDirect
|
||||
}
|
||||
|
||||
// messageKind maps a KOOK content-kind onto the adapter ContentType and the
|
||||
// media list (media URL is carried by the content field for image/video/file/
|
||||
// audio messages).
|
||||
func messageKind(kind int, content string) (adapter.ContentType, []adapter.Media) {
|
||||
switch kind {
|
||||
case kindImage:
|
||||
return adapter.ContentImage, []adapter.Media{{Kind: adapter.MediaImage, URL: content}}
|
||||
case kindVideo:
|
||||
return adapter.ContentFile, []adapter.Media{{Kind: adapter.MediaVideo, URL: content}}
|
||||
case kindFile:
|
||||
return adapter.ContentFile, []adapter.Media{{Kind: adapter.MediaFile, URL: content}}
|
||||
case kindAudio:
|
||||
return adapter.ContentVoice, []adapter.Media{{Kind: adapter.MediaVoice, URL: content}}
|
||||
default:
|
||||
return adapter.ContentText, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mentionsFrom converts an array of mentioned user ids into adapter.Mention.
|
||||
func mentionsFrom(ids []string) []adapter.Mention {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]adapter.Mention, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id != "" {
|
||||
out = append(out, adapter.Mention{UserID: id})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// asString decodes a raw JSON value that is expected to be a string, returning
|
||||
// "" when it is not.
|
||||
func asString(raw json.RawMessage) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err != nil {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in new issue
Block a user