feat(kook): P2 KOOK 专属页+后端 admin 端点(design/17 §2/§4)
后端(KOOK REST, 复用 httpclient WithAuthScheme(Bot), token不打印): - kook/admin.go(新): ListFriends/HandleFriendRequest/DeleteFriend/BlockFriend/ ListUserChats/CreateUserChat/ListDirectMessages - kook/admin_guild.go(新): ListGuilds/ListGuildChannels/ListGuildUsers/ListChannelMsgs/SendTestMessage - gateway/admin_channels_platform.go(新): kookAdmin(反射解包namedAdapter命中内嵌PlatformAdapter) + 4 handler + adminKookView聚合(子项失败记errors不伪造) - server.go 注册 4 条 ownerAuth 路由: /view /friends /friend/handle /message/test - admin.go/admin_write_channel_config.go: AdminChannelAccount 加 Capabilities 前端(KOOK 专属页): - KookPanel.tsx(新): 好友&申请/服务器/频道/成员/私信会话/发消息测试卡片 - AdapterDetailPage.tsx: platform==='kook' 渲染 KookPanel, 否则通用 - types.ts/api.ts: KOOK 类型 + adminChannelPlatformView/Friends/FriendHandle/MessageTest 文档: Docs/api-client.md 新增4端点契约(owner-only)+capabilities+v0.17 验收: go build/vet/test ./... 全绿 + npm run build 97模块全绿; 零新依赖 注: api.ts/types.ts 集中契约文件>500行属既有结构(未新增违规); KOOK其余REST方法留能力面待用
This commit is contained in:
1 parent
8eaf81bd0d
commit
5ec4c9814d
13 files changed
+1714
-2
No files matched your search
@@ -198,7 +198,9 @@ func (s *Server) maskedChannelAccounts() map[string][]AdminChannelAccount {
|
||||
if !configuredAccount(a) {
|
||||
continue
|
||||
}
|
||||
list = append(list, adminChannelAccountOf(runtimeconfig.Masked(a)))
|
||||
account := adminChannelAccountOf(runtimeconfig.Masked(a))
|
||||
account.Capabilities = platformAdminOps(platform)
|
||||
list = append(list, account)
|
||||
}
|
||||
if len(list) > 0 {
|
||||
out[platform] = list
|
||||
@@ -207,6 +209,16 @@ func (s *Server) maskedChannelAccounts() map[string][]AdminChannelAccount {
|
||||
return out
|
||||
}
|
||||
|
||||
// platformAdminOps returns the account-level admin operations the platform's
|
||||
// adapter exposes, so the panel renders only the capabilities the account really
|
||||
// has. Platforms without a specialized adapter surface return nil (omitted).
|
||||
func platformAdminOps(platform string) []string {
|
||||
if channelBasePlatform(platform) == "kook" {
|
||||
return []string{"friends", "user_chats", "channels", "guilds", "message_test"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasChannelSummary reports whether out already contains a summary for platform
|
||||
// (so a platform with a registered adapter is not duplicated).
|
||||
func (s *Server) hasChannelSummary(out []ChannelSummary, platform string) bool {
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
package gateway
|
||||
|
||||
// This file adds the per-platform adapter panel endpoints (design/17 §2), owner
|
||||
// only and audited. They expose a platform's real account data (friends,
|
||||
// channels, guilds, private chats) via the adapter's REST surface and a single
|
||||
// controlled "test send" write. Only KOOK is wired; every other platform answers
|
||||
// an honest 501 rather than fabricated data (a project red line). The bot token
|
||||
// is never read or logged — the adapter owns the token and surfaces only its
|
||||
// normalized REST results.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/kook"
|
||||
)
|
||||
|
||||
// kookAdmin is the subset of the KOOK adapter's admin REST surface the gateway
|
||||
// panel endpoints invoke. It is implemented by *kook.Adapter and reached by
|
||||
// unwrapping the registration wrapper.
|
||||
type kookAdmin interface {
|
||||
ListFriends(ctx context.Context, typ string) (kook.FriendList, error)
|
||||
HandleFriendRequest(ctx context.Context, id int64, accept bool) error
|
||||
ListUserChats(ctx context.Context, page, pageSize int) ([]kook.UserChat, error)
|
||||
CreateUserChat(ctx context.Context, targetID string) (kook.UserChat, error)
|
||||
ListDirectMessages(ctx context.Context, chatCode, targetID string, page, pageSize int) ([]kook.DirectMessage, error)
|
||||
ListGuilds(ctx context.Context, page, pageSize int) ([]kook.Guild, error)
|
||||
ListGuildChannels(ctx context.Context, guildID string, typ int) ([]kook.Channel, error)
|
||||
ListGuildUsers(ctx context.Context, guildID string, page, pageSize int) ([]kook.GuildUser, error)
|
||||
ListChannelMsgs(ctx context.Context, targetID string, pageSize int) ([]kook.ChannelMessage, error)
|
||||
SendTestMessage(ctx context.Context, targetID, content string) (kook.TestMessageResult, error)
|
||||
}
|
||||
|
||||
// adminKookView is the wire shape of GET /api/v1/admin/channels/{platform}/view.
|
||||
type adminKookView struct {
|
||||
Platform string `json:"platform"`
|
||||
AccountID string `json:"accountId,omitempty"`
|
||||
Friends *kook.FriendList `json:"friends,omitempty"`
|
||||
UserChats []kook.UserChat `json:"userChats,omitempty"`
|
||||
Guilds []kook.Guild `json:"guilds,omitempty"`
|
||||
Channels []kook.Channel `json:"channels,omitempty"`
|
||||
Members []kook.GuildUser `json:"members,omitempty"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// kookAccountFor resolves the *kook.Adapter registered for a platform/account.
|
||||
// It is nil for a non-kook platform, an unknown account, or a wrapped adapter
|
||||
// that does not expose the admin surface.
|
||||
func kookAccountFor(caps CapabilityStore, platform, accountID string) (kookAdmin, bool) {
|
||||
if caps == nil {
|
||||
return nil, false
|
||||
}
|
||||
for _, cap := range caps.List(capreg.KindChannel) {
|
||||
ch, ok := cap.Value.(sdk.Channel)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
plat, acct := splitAdapterName(ch.Name())
|
||||
if plat != platform {
|
||||
continue
|
||||
}
|
||||
if accountID != "" && acct != accountID {
|
||||
continue
|
||||
}
|
||||
if k, ok := asKookAdmin(cap.Value); ok {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// asKookAdmin unwraps a registered channel value to its inner KOOK adapter. The
|
||||
// registration wraps the adapter in cmd/cyrene's namedAdapter (an unexported
|
||||
// struct embedding adapter.PlatformAdapter); the embedded field is exported, so
|
||||
// reflection reads it and type-asserts to the KOOK surface. A direct *kook.Adapter
|
||||
// value (tests) is also accepted.
|
||||
func asKookAdmin(v any) (kookAdmin, bool) {
|
||||
if k, ok := v.(kookAdmin); ok {
|
||||
return k, true
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
if !rv.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return nil, false
|
||||
}
|
||||
f := rv.FieldByName("PlatformAdapter")
|
||||
if !f.IsValid() || !f.CanInterface() {
|
||||
return nil, false
|
||||
}
|
||||
if k, ok := f.Interface().(kookAdmin); ok {
|
||||
return k, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// adminChannelPlatformView aggregates a KOOK account's friends, private chats and
|
||||
// guilds (with the first guild's channels). Each sub-fetch is best-effort; a
|
||||
// failing one is recorded in errors rather than aborting the whole view.
|
||||
func (s *Server) adminChannelPlatformView(w http.ResponseWriter, r *http.Request) {
|
||||
platform := r.PathValue("platform")
|
||||
accountID := r.URL.Query().Get("accountId")
|
||||
k, found := s.kookTarget(r, w, "channel.platform.view", platform, accountID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
view := s.buildKookView(r.Context(), k, platform, accountID)
|
||||
s.auditClaims(r, "channel.platform.view", "ok")
|
||||
writeJSON(w, http.StatusOK, view)
|
||||
}
|
||||
|
||||
func (s *Server) buildKookView(ctx context.Context, k kookAdmin, platform, accountID string) adminKookView {
|
||||
view := adminKookView{Platform: platform, AccountID: accountID, Errors: map[string]string{}}
|
||||
if fl, err := k.ListFriends(ctx, kook.FriendTypeAll); err != nil {
|
||||
view.Errors["friends"] = err.Error()
|
||||
} else {
|
||||
view.Friends = &fl
|
||||
}
|
||||
if chats, err := k.ListUserChats(ctx, 1, 50); err != nil {
|
||||
view.Errors["user_chats"] = err.Error()
|
||||
} else {
|
||||
view.UserChats = chats
|
||||
}
|
||||
if guilds, err := k.ListGuilds(ctx, 1, 50); err != nil {
|
||||
view.Errors["guilds"] = err.Error()
|
||||
} else {
|
||||
view.Guilds = guilds
|
||||
if len(guilds) > 0 {
|
||||
// Aggregate the first guild's channels and members so the panel has a
|
||||
// concrete server to render; deeper servers are a separate fetch.
|
||||
if channels, err := k.ListGuildChannels(ctx, guilds[0].ID, -1); err != nil {
|
||||
view.Errors["channels"] = err.Error()
|
||||
} else {
|
||||
view.Channels = channels
|
||||
}
|
||||
if members, err := k.ListGuildUsers(ctx, guilds[0].ID, 1, 50); err != nil {
|
||||
view.Errors["members"] = err.Error()
|
||||
} else {
|
||||
view.Members = members
|
||||
}
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// adminChannelPlatformFriends returns a KOOK account's friends/requests/blocked
|
||||
// list, filtered by the optional ?type= and ?accountId= params.
|
||||
func (s *Server) adminChannelPlatformFriends(w http.ResponseWriter, r *http.Request) {
|
||||
platform := r.PathValue("platform")
|
||||
accountID := r.URL.Query().Get("accountId")
|
||||
k, found := s.kookTarget(r, w, "channel.friends", platform, accountID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
fl, err := k.ListFriends(r.Context(), r.URL.Query().Get("type"))
|
||||
if err != nil {
|
||||
s.auditClaims(r, "channel.friends", "error")
|
||||
s.log.Error("kook friends", "platform", platform, "err", err)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "kook_friends_failed", "拉取好友列表失败:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
s.auditClaims(r, "channel.friends", "ok")
|
||||
writeJSON(w, http.StatusOK, fl)
|
||||
}
|
||||
|
||||
// kookFriendHandleRequest is the POST /admin/channels/{platform}/friend/handle body.
|
||||
type kookFriendHandleRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Accept bool `json:"accept"`
|
||||
}
|
||||
|
||||
// adminChannelFriendHandle accepts or rejects a KOOK friend request (owner write).
|
||||
func (s *Server) adminChannelFriendHandle(w http.ResponseWriter, r *http.Request) {
|
||||
platform := r.PathValue("platform")
|
||||
accountID := r.URL.Query().Get("accountId")
|
||||
k, found := s.kookTarget(r, w, "channel.friend.handle", platform, accountID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
var req kookFriendHandleRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
s.auditClaims(r, "channel.friend.handle", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "id is required"))
|
||||
return
|
||||
}
|
||||
if err := k.HandleFriendRequest(r.Context(), req.ID, req.Accept); err != nil {
|
||||
s.auditClaims(r, "channel.friend.handle", "error")
|
||||
s.log.Error("kook friend handle", "platform", platform, "id", req.ID, "err", err)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "kook_friend_handle_failed", "处理好友申请失败:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
s.auditClaims(r, "channel.friend.handle", "ok")
|
||||
writeJSON(w, http.StatusOK, map[string]any{"handled": true, "id": req.ID, "accept": req.Accept})
|
||||
}
|
||||
|
||||
// kookMessageTestRequest is the POST /admin/channels/{platform}/message/test body.
|
||||
type kookMessageTestRequest struct {
|
||||
TargetID string `json:"targetId"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// adminChannelMessageTest sends one test message to a KOOK target channel (owner
|
||||
// write), so the panel can confirm the bot can reach the channel.
|
||||
func (s *Server) adminChannelMessageTest(w http.ResponseWriter, r *http.Request) {
|
||||
platform := r.PathValue("platform")
|
||||
accountID := r.URL.Query().Get("accountId")
|
||||
k, found := s.kookTarget(r, w, "channel.message.test", platform, accountID)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
var req kookMessageTestRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.TargetID) == "" {
|
||||
s.auditClaims(r, "channel.message.test", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "targetId is required"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Content) == "" {
|
||||
s.auditClaims(r, "channel.message.test", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "content is required"))
|
||||
return
|
||||
}
|
||||
res, err := k.SendTestMessage(r.Context(), strings.TrimSpace(req.TargetID), strings.TrimSpace(req.Content))
|
||||
if err != nil {
|
||||
s.auditClaims(r, "channel.message.test", "error")
|
||||
s.log.Error("kook message test", "platform", platform, "target", req.TargetID, "err", err)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "kook_message_test_failed", "发送测试消息失败:%s", err.Error()))
|
||||
return
|
||||
}
|
||||
s.auditClaims(r, "channel.message.test", "ok")
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
// kookTarget resolves the KOOK adapter for a platform/account, writing an honest
|
||||
// error and reporting found=false when it cannot.
|
||||
func (s *Server) kookTarget(r *http.Request, w http.ResponseWriter, what, platform, accountID string) (kookAdmin, bool) {
|
||||
platform = strings.TrimSpace(platform)
|
||||
if platform != "kook" {
|
||||
s.auditClaims(r, what, "error")
|
||||
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "仅 KOOK 平台实现了适配器面板数据端点", HTTPCode: http.StatusNotImplemented})
|
||||
return nil, false
|
||||
}
|
||||
k, found := kookAccountFor(s.caps, platform, accountID)
|
||||
if !found {
|
||||
s.auditClaims(r, what, "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "kook 账号 %q 未找到", accountID))
|
||||
return nil, false
|
||||
}
|
||||
return k, true
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package gateway
|
||||
|
||||
// Exercises the per-platform KOOK adapter panel endpoints (design/17 §2). The
|
||||
// adapter is registered through a wrapper that embeds adapter.PlatformAdapter —
|
||||
// mirroring cmd/cyrene's namedAdapter — so these tests also cover the reflection
|
||||
// unwrap the gateway uses to reach the concrete *kook.Adapter.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/kook"
|
||||
)
|
||||
|
||||
func gatewayKookLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
// gatewayKookWrap embeds adapter.PlatformAdapter the way cmd/cyrene's namedAdapter
|
||||
// does, so the gateway's reflection-based unwrap (asKookAdmin) is exercised.
|
||||
type gatewayKookWrap struct {
|
||||
adapter.PlatformAdapter
|
||||
name string
|
||||
}
|
||||
|
||||
func (w *gatewayKookWrap) Name() string { return w.name }
|
||||
|
||||
// gatewayKookServer answers the KOOK REST surface a panel view touches.
|
||||
func gatewayKookServer(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/friend":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
|
||||
"friend": []any{map[string]any{"id": 1, "friend_info": map[string]any{"id": "u1", "username": "alice"}}},
|
||||
}})
|
||||
case "/user-chat/list":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
|
||||
"items": []any{map[string]any{"code": "c1", "target_info": map[string]any{"id": "u9", "username": "zed"}}},
|
||||
}})
|
||||
case "/guild/list":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
|
||||
"items": []any{map[string]any{"id": "g1", "name": "server"}},
|
||||
}})
|
||||
case "/channel/list":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
|
||||
"items": []any{map[string]any{"id": "c1", "guild_id": "g1", "name": "general", "type": 1}},
|
||||
}})
|
||||
case "/guild/user-list":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
|
||||
"items": []any{map[string]any{"id": "u1", "username": "alice", "online": true}},
|
||||
}})
|
||||
case "/friend/handle-request":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{}})
|
||||
case "/message/create":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{"msg_id": "m1"}})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(h)
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
// newKookGateway wires a registry whose kook channel wraps a real adapter pointed
|
||||
// at an httptest KOOK server, and returns the owner JWT.
|
||||
func newKookGateway(t *testing.T, name string) (string, string) {
|
||||
t.Helper()
|
||||
kookSrv := gatewayKookServer(t)
|
||||
ka := kook.New(
|
||||
kook.Config{BotToken: "testtoken"},
|
||||
kook.WithBaseURL(kookSrv.URL),
|
||||
kook.WithHTTPClient(kookSrv.Client()),
|
||||
kook.WithLogger(gatewayKookLogger()),
|
||||
)
|
||||
reg := registry.New()
|
||||
if err := reg.RegisterChannel(&gatewayKookWrap{PlatformAdapter: ka, name: name}); err != nil {
|
||||
t.Fatalf("register channel: %v", err)
|
||||
}
|
||||
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{Registry: reg})
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
return ts.URL, tok
|
||||
}
|
||||
|
||||
func TestAdminChannelPlatformView_Kook(t *testing.T) {
|
||||
t.Parallel()
|
||||
base, tok := newKookGateway(t, "kook:conf0")
|
||||
|
||||
resp, body := doReq(t, http.MethodGet, base+"/api/v1/admin/channels/kook/view?accountId=conf0", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("view status = %d, body=%s", resp.StatusCode, body)
|
||||
}
|
||||
var out struct {
|
||||
Data struct {
|
||||
Platform string `json:"platform"`
|
||||
Friends *struct {
|
||||
Friends []struct{ UserID string `json:"user_id"` } `json:"friends"`
|
||||
} `json:"friends"`
|
||||
Guilds []struct{ ID string `json:"id"` } `json:"guilds"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Fatalf("decode view: %v", err)
|
||||
}
|
||||
if out.Data.Platform != "kook" {
|
||||
t.Errorf("platform = %q, want kook", out.Data.Platform)
|
||||
}
|
||||
if out.Data.Friends == nil || len(out.Data.Friends.Friends) != 1 || out.Data.Friends.Friends[0].UserID != "u1" {
|
||||
t.Errorf("friends = %+v, want [u1]", out.Data.Friends)
|
||||
}
|
||||
if len(out.Data.Guilds) != 1 || out.Data.Guilds[0].ID != "g1" {
|
||||
t.Errorf("guilds = %+v, want [g1]", out.Data.Guilds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminChannelPlatformView_NonKookNotImplemented(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{})
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/telegram/view", nil, tok)
|
||||
if resp.StatusCode != http.StatusNotImplemented {
|
||||
t.Fatalf("status = %d, want 501 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminChannelFriendHandle_Kook(t *testing.T) {
|
||||
t.Parallel()
|
||||
base, tok := newKookGateway(t, "kook:conf0")
|
||||
|
||||
resp, body := doReq(t, http.MethodPost, base+"/api/v1/admin/channels/kook/friend/handle?accountId=conf0",
|
||||
map[string]any{"id": 77, "accept": true}, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("handle status = %d, body=%s", resp.StatusCode, body)
|
||||
}
|
||||
var out struct {
|
||||
Data struct {
|
||||
Handled bool `json:"handled"`
|
||||
ID int64 `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Fatalf("decode handle: %v", err)
|
||||
}
|
||||
if !out.Data.Handled || out.Data.ID != 77 {
|
||||
t.Errorf("handle = %+v, want handled id 77", out.Data)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,11 @@ type AdminChannelAccount struct {
|
||||
BotID string `json:"botId,omitempty"`
|
||||
BotUsername string `json:"botUsername,omitempty"`
|
||||
BotNickname string `json:"botNickname,omitempty"`
|
||||
// Capabilities lists the account-level admin operations the platform's
|
||||
// adapter exposes (friends/channels/user-chats/message-test) so the panel
|
||||
// renders the right per-account cards. It is omitted for platforms without a
|
||||
// specialized adapter surface.
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// channelConfigRequest is the PUT /api/v1/admin/channels/{platform}/config body.
|
||||
|
||||
@@ -418,6 +418,10 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("POST /api/v1/admin/channels/open", ownerAuth(http.HandlerFunc(s.adminChannelOpen)))
|
||||
mux.Handle("POST /api/v1/admin/channels/close", ownerAuth(http.HandlerFunc(s.adminChannelClose)))
|
||||
mux.Handle("POST /api/v1/admin/channels/test", ownerAuth(http.HandlerFunc(s.adminChannelTest)))
|
||||
mux.Handle("GET /api/v1/admin/channels/{platform}/view", ownerAuth(http.HandlerFunc(s.adminChannelPlatformView)))
|
||||
mux.Handle("GET /api/v1/admin/channels/{platform}/friends", ownerAuth(http.HandlerFunc(s.adminChannelPlatformFriends)))
|
||||
mux.Handle("POST /api/v1/admin/channels/{platform}/friend/handle", ownerAuth(http.HandlerFunc(s.adminChannelFriendHandle)))
|
||||
mux.Handle("POST /api/v1/admin/channels/{platform}/message/test", ownerAuth(http.HandlerFunc(s.adminChannelMessageTest)))
|
||||
mux.Handle("PUT /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminSaveChannelConfig)))
|
||||
mux.Handle("DELETE /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminDeleteChannelAccount)))
|
||||
mux.Handle("POST /api/v1/admin/plugins/install", ownerAuth(http.HandlerFunc(s.adminPluginInstall)))
|
||||
|
||||
Reference in new issue
Block a user