feat(channel): KOOK Webhook(WH) 接收模式——与 WS 配置切换(ReceiveMode)
- kook.Config 加 ReceiveMode("ws"默认/"webhook") + WebhookPath(默认/webhook/kook) + WebhookVerifyToken + WebhookEncryptKey(AES-256-CBC)
- 按权威文档(Reference/kaiheila webhook.md)实现: 无X-Signature HMAC, 真实校验=d.verify_token比对(不匹配403)+可选AES解密+zlib解压+challenge回显
- webhook.go: InboundHandler+WebhookPath, POST→zlib解压→AES解密→verify_token校验→challenge回显→归一UnifiedMessage→SetMessageHandler; WS模式Connect不拨网关仅标记connected
- channelbridge/webhookAdapter 接口改用 WebhookPath(); WebhookHandlers 收KOOK WH(ws模式跳过); 既有webhook通道补WebhookPath照常挂载
- namedAdapter 转发InboundHandler/WebhookPath; kook env兜底 +4个CHANNEL_KOOK_*键
- 测试: verify_token 403/challenge/zlib/AES往返/WebhookPath按模式/WH不拨gateway/WebhookHandlers收KOOK; 全仓build/vet/test绿
- 待真token端到端实测(需主人提供CHANNEL_KOOK_TOKEN+开发者后台切WebHook配回调URL)
This commit is contained in:
1 parent
1ebc82fffd
commit
617157262b
11 files changed
+981
-32
No files matched your search
+26
-2
@@ -17,6 +17,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
@@ -91,8 +92,12 @@ func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *
|
||||
func channelAccountsFromEnv() map[string][]config.ChannelAccount {
|
||||
out := map[string][]config.ChannelAccount{}
|
||||
out["kook"] = append(out["kook"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": os.Getenv("CHANNEL_KOOK_TOKEN"),
|
||||
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
|
||||
"bot_token": os.Getenv("CHANNEL_KOOK_TOKEN"),
|
||||
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
|
||||
"receive_mode": os.Getenv("CHANNEL_KOOK_RECEIVE_MODE"),
|
||||
"webhook_path": os.Getenv("CHANNEL_KOOK_WEBHOOK_PATH"),
|
||||
"webhook_verify_token": os.Getenv("CHANNEL_KOOK_WEBHOOK_VERIFY_TOKEN"),
|
||||
"webhook_encrypt_key": os.Getenv("CHANNEL_KOOK_WEBHOOK_ENCRYPT_KEY"),
|
||||
}})
|
||||
out["telegram"] = append(out["telegram"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"),
|
||||
@@ -166,6 +171,25 @@ func (n *namedAdapter) Config() map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// InboundHandler forwards to the inner adapter when it exposes an inbound HTTP
|
||||
// callback; the bridge type-asserts the wrapper and mounts the handler at
|
||||
// WebhookPath. It returns nil for a non-webhook inner adapter.
|
||||
func (n *namedAdapter) InboundHandler() http.Handler {
|
||||
if inner, ok := n.PlatformAdapter.(interface{ InboundHandler() http.Handler }); ok {
|
||||
return inner.InboundHandler()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebhookPath forwards the inner adapter's webhook route, returning "" when the
|
||||
// inner adapter does not expose one (so the bridge skips it).
|
||||
func (n *namedAdapter) WebhookPath() string {
|
||||
if inner, ok := n.PlatformAdapter.(interface{ WebhookPath() string }); ok {
|
||||
return inner.WebhookPath()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildAccountAdapter builds one account's adapter for a platform, decoding the
|
||||
// account's generic Config map into the platform's typed config and wrapping it so
|
||||
// the bridge sees a distinct account name and its allow/deny lists.
|
||||
|
||||
@@ -97,6 +97,52 @@ func TestChannelFactoryBuildFromFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNamedAdapterForwardsWebhook verifies the production wrapper forwards a
|
||||
// kook webhook-mode adapter's InboundHandler/WebhookPath to the channel bridge,
|
||||
// so bridge.WebhookHandlers() mounts the KOOK callback route on the gateway.
|
||||
func TestNamedAdapterForwardsWebhook(t *testing.T) {
|
||||
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
|
||||
t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "webhook")
|
||||
t.Setenv("CHANNEL_KOOK_WEBHOOK_PATH", "/webhook/kook")
|
||||
|
||||
adapters := buildChannelAdapters(testFactory(t))
|
||||
var found *namedAdapter
|
||||
for _, a := range adapters {
|
||||
if nwa, ok := a.(*namedAdapter); ok && barePlatform(nwa.Name()) == "kook" {
|
||||
found = nwa
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatalf("kook named adapter not found, got %v", names(adapters))
|
||||
}
|
||||
if p := found.WebhookPath(); p != "/webhook/kook" {
|
||||
t.Errorf("WebhookPath = %q, want /webhook/kook", p)
|
||||
}
|
||||
if found.InboundHandler() == nil {
|
||||
t.Error("InboundHandler() = nil, want a mounted handler")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNamedAdapterNoWebhookInWSMode verifies a ws-mode adapter (all non-webhook
|
||||
// platforms) does not surface a webhook handler through the wrapper.
|
||||
func TestNamedAdapterNoWebhookInWSMode(t *testing.T) {
|
||||
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
|
||||
t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "ws")
|
||||
|
||||
adapters := buildChannelAdapters(testFactory(t))
|
||||
for _, a := range adapters {
|
||||
if barePlatform(a.Name()) != "kook" {
|
||||
continue
|
||||
}
|
||||
if nwa, ok := a.(*namedAdapter); ok {
|
||||
if p := nwa.WebhookPath(); p != "" {
|
||||
t.Errorf("ws-mode kook WebhookPath = %q, want empty", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func names(adapters []adapter.PlatformAdapter) []string {
|
||||
out := make([]string, 0, len(adapters))
|
||||
for _, a := range adapters {
|
||||
|
||||
Reference in new issue
Block a user