package snowluma import ( "bytes" "context" "crypto/hmac" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net" "net/http" "sync" "time" ) // HTTPReport HTTP POST 上报连接器:框架起一个 HTTP 服务, // SnowLuma 把事件 POST 过来;处理函数里调用的 Reply/Approve 会以 // OneBot「快速操作」的形式随 HTTP 响应返回。 type HTTPReport struct { addr string srv *http.Server } // NewHTTPReport 创建 HTTP 上报连接器,监听 addr(如 ":8081"), // SnowLuma 侧上报地址填 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 校验 SnowLuma 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 到 SnowLuma 的 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 /,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("snowluma: 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("snowluma: http api %s: %w", action, err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return fmt.Errorf("snowluma: action %s not found (404)", action) } var envelope Response if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil { return fmt.Errorf("snowluma: 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("snowluma: decode data of %s: %w", action, err) } return nil }