193 lines
5.2 KiB
Go
193 lines
5.2 KiB
Go
package napcat
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/hmac"
|
||
"crypto/sha1"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// HTTPReport HTTP POST 上报连接器:框架起一个 HTTP 服务,
|
||
// NapCat 把事件 POST 过来;处理函数里调用的 Reply/Approve 会以
|
||
// OneBot「快速操作」的形式随 HTTP 响应返回。
|
||
type HTTPReport struct {
|
||
addr string
|
||
srv *http.Server
|
||
}
|
||
|
||
// NewHTTPReport 创建 HTTP 上报连接器,监听 addr(如 ":8081"),
|
||
// NapCat 侧上报地址填 http://<本机IP>:8081/(任意路径均可)。
|
||
func NewHTTPReport(addr string) *HTTPReport { return &HTTPReport{addr: addr} }
|
||
|
||
func (h *HTTPReport) Name() string { return "http-report(" + h.addr + ")" }
|
||
|
||
func (h *HTTPReport) Serve(ctx context.Context, bot *Client) error {
|
||
mux := http.NewServeMux()
|
||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
body, err := readAllLimited(r.Body)
|
||
if err != nil {
|
||
http.Error(w, "read body: "+err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
if bot.secret != "" && !verifySignature(bot.secret, body, r.Header.Get("X-Signature")) {
|
||
http.Error(w, "invalid signature", http.StatusForbidden)
|
||
return
|
||
}
|
||
ev, err := decodeEvent(body)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
quick := &quickBox{params: map[string]any{}}
|
||
bot.dispatch(ev, bot, quick)
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(quick.params)
|
||
})
|
||
srv := &http.Server{
|
||
Addr: h.addr,
|
||
Handler: mux,
|
||
BaseContext: func(net.Listener) context.Context { return ctx },
|
||
}
|
||
h.srv = srv
|
||
err := srv.ListenAndServe()
|
||
if errors.Is(err, http.ErrServerClosed) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
// Shutdown 关闭监听(Run 的退出流程会调用)。
|
||
func (h *HTTPReport) Shutdown(ctx context.Context) error {
|
||
if h.srv == nil {
|
||
return nil
|
||
}
|
||
return h.srv.Shutdown(ctx)
|
||
}
|
||
|
||
// quickBox 收集 HTTP 上报模式下随响应返回的快速操作参数。
|
||
type quickBox struct {
|
||
params map[string]any
|
||
}
|
||
|
||
func (q *quickBox) set(key string, value any) {
|
||
if q.params == nil {
|
||
q.params = map[string]any{}
|
||
}
|
||
q.params[key] = value
|
||
}
|
||
|
||
// verifySignature 校验 NapCat HTTP 上报的 X-Signature(HMAC-SHA1)。
|
||
func verifySignature(secret string, body []byte, got string) bool {
|
||
mac := hmac.New(sha1.New, []byte(secret))
|
||
mac.Write(body)
|
||
want := "sha1=" + hex.EncodeToString(mac.Sum(nil))
|
||
return hmac.Equal([]byte(got), []byte(want))
|
||
}
|
||
|
||
func readAllLimited(r io.Reader) ([]byte, error) {
|
||
const maxBody = 32 << 20
|
||
buf := &bytes.Buffer{}
|
||
if _, err := io.Copy(buf, io.LimitReader(r, maxBody+1)); err != nil {
|
||
return nil, err
|
||
}
|
||
if buf.Len() > maxBody {
|
||
return nil, errors.New("request body too large")
|
||
}
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
// HTTPAPI HTTP API 连接器:框架作为客户端,POST 到 NapCat 的 HTTP 服务执行 action。
|
||
// 仅配置了 HTTPAPI 而没有 WS 会话时,Client.Call 走这条通道。
|
||
type HTTPAPI struct {
|
||
base string
|
||
token atomicString
|
||
hc *http.Client
|
||
}
|
||
|
||
type atomicString struct {
|
||
mu sync.RWMutex
|
||
v string
|
||
}
|
||
|
||
func (a *atomicString) Store(v string) {
|
||
a.mu.Lock()
|
||
a.v = v
|
||
a.mu.Unlock()
|
||
}
|
||
|
||
func (a *atomicString) Load() string {
|
||
a.mu.RLock()
|
||
defer a.mu.RUnlock()
|
||
return a.v
|
||
}
|
||
|
||
// NewHTTPAPI 创建 HTTP API 连接器,base 形如 http://127.0.0.1:3000。
|
||
func NewHTTPAPI(base string) *HTTPAPI {
|
||
return &HTTPAPI{base: base, hc: &http.Client{Timeout: 30 * time.Second}}
|
||
}
|
||
|
||
func (h *HTTPAPI) Name() string { return "http-api(" + h.base + ")" }
|
||
|
||
// Serve 对 HTTP API 连接器只是完成鉴权配置并阻塞到框架退出。
|
||
func (h *HTTPAPI) Serve(ctx context.Context, bot *Client) error {
|
||
h.token.Store(bot.token)
|
||
<-ctx.Done()
|
||
return nil
|
||
}
|
||
|
||
// Call 实现 Caller:POST <base>/<action>,body 为 params 对象。
|
||
func (h *HTTPAPI) 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()
|
||
}
|
||
body := []byte("{}")
|
||
if params != nil {
|
||
var err error
|
||
if body, err = json.Marshal(params); err != nil {
|
||
return fmt.Errorf("napcat: marshal params of %s: %w", action, err)
|
||
}
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.base+"/"+action, bytes.NewReader(body))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
if token := h.token.Load(); token != "" {
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
}
|
||
resp, err := h.hc.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("napcat: http api %s: %w", action, err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode == http.StatusNotFound {
|
||
return fmt.Errorf("napcat: action %s not found (404)", action)
|
||
}
|
||
var envelope Response
|
||
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
|
||
return fmt.Errorf("napcat: http api %s: bad response (http %d): %w", action, resp.StatusCode, err)
|
||
}
|
||
if err := envelope.Err(); err != nil {
|
||
return err
|
||
}
|
||
if err := envelope.Decode(out); err != nil {
|
||
return fmt.Errorf("napcat: decode data of %s: %w", action, err)
|
||
}
|
||
return nil
|
||
}
|