Files
napcat/caller.go

128 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package napcat
import (
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"sync/atomic"
"time"
)
// Caller 是一条可执行 OneBot action 的通道(WS 会话或 HTTP API)。
type Caller interface {
Call(ctx context.Context, action string, params any, out any) error
}
// CallAPI 通过指定 Caller 调用一个 action 并把 data 解码为 T。
// ctx 为 nil 时使用 30 秒超时。
func CallAPI[T any](c Caller, ctx context.Context, action string, params any) (T, error) {
var zero T
if c == nil {
return zero, ErrNotConnected
}
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
}
var out T
if err := c.Call(ctx, action, params, &out); err != nil {
return zero, err
}
return out, nil
}
// wsRequest 是 WS 上发出的 action 请求。
type wsRequest struct {
Action string `json:"action"`
Params any `json:"params,omitempty"`
Echo string `json:"echo"`
}
// wsCaller 基于一条 WebSocket 连接实现 Caller:
// 发出的请求带自增 echo,读循环把响应按 echo 路由回等待方。
type wsCaller struct {
wmu sync.Mutex
writer messageWriter
pending sync.Map // echo(string) -> chan Response
seq atomic.Int64
done <-chan struct{} // 连接关闭时被关闭
}
// messageWriter 抽象 gorilla/websocket 的写方法,便于单测替换。
type messageWriter interface {
WriteMessage(messageType int, data []byte) error
}
func newWSCaller(w messageWriter, done <-chan struct{}) *wsCaller {
return &wsCaller{writer: w, done: done}
}
func (w *wsCaller) Call(ctx context.Context, action string, params any, out any) error {
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
}
echo := strconv.FormatInt(w.seq.Add(1), 10)
payload, err := json.Marshal(wsRequest{Action: action, Params: params, Echo: echo})
if err != nil {
return fmt.Errorf("napcat: marshal action %s: %w", action, err)
}
ch := make(chan Response, 1)
w.pending.Store(echo, ch)
defer w.pending.Delete(echo)
w.wmu.Lock()
err = w.writer.WriteMessage(1 /* websocket.TextMessage */, payload)
w.wmu.Unlock()
if err != nil {
return fmt.Errorf("napcat: send action %s: %w", action, err)
}
select {
case resp := <-ch:
if err := resp.Err(); err != nil {
return err
}
if err := resp.Decode(out); err != nil {
return fmt.Errorf("napcat: decode data of %s: %w", action, err)
}
return nil
case <-ctx.Done():
return ctx.Err()
case <-w.done:
return ErrNotConnected
}
}
// routeResponse 由读循环调用:把带 echo 的响应分发给等待者,返回是否路由成功。
func (w *wsCaller) routeResponse(data []byte, log logger) bool {
var resp Response
if err := json.Unmarshal(data, &resp); err != nil {
log.Warn("napcat: unmarshal response failed", "err", err)
return false
}
v, ok := w.pending.Load(resp.Echo)
if !ok {
log.Warn("napcat: no pending action for echo", "echo", resp.Echo)
return false
}
ch := v.(chan Response)
select {
case ch <- resp:
default:
}
return true
}
// logger 是框架依赖的最小日志接口(*slog.Logger 满足它)。
type logger interface {
Warn(msg string, args ...any)
Info(msg string, args ...any)
Error(msg string, args ...any)
Debug(msg string, args ...any)
}