napcat: NapCatQQ (OneBot 11) Go SDK, initial release
This commit is contained in:
192
connect_http.go
Normal file
192
connect_http.go
Normal file
@ -0,0 +1,192 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user