mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-09-05 07:02:06 +08:00
* feat: afterMsgSaveDB webhook fix(config): add default webhook event filters fix(core): handle group mentions and webhook enqueue errors Use group IDs when filtering group-message attention callbacks, and report failures when asynchronous webhook tasks cannot be queued or posted. feat(webhook): add standalone mock webhook server CLI test(webhook): add mock server and test suite for all callback commands feat(msgtransfer): trigger afterMsgSaveDB webhook when message saved to DB feat(config): add afterMsgSaveDB webhook configuration feat(callbackstruct): add CallbackAfterMsgSaveDB command and request structs * refactor(core): simplify asynchronous webhook enqueueing
185 lines
4.4 KiB
Go
185 lines
4.4 KiB
Go
// Copyright © 2023 OpenIM. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
}
|
|
|
|
// RequestRecord stores information about a received webhook request.
|
|
type RequestRecord struct {
|
|
Timestamp time.Time
|
|
Command string
|
|
Body []byte
|
|
Headers http.Header
|
|
}
|
|
|
|
// Server is a mock webhook HTTP server for OpenIM callbacks.
|
|
type Server struct {
|
|
Engine *gin.Engine
|
|
handlers map[string]gin.HandlerFunc
|
|
mu sync.RWMutex
|
|
httpServer *http.Server
|
|
recordHistory bool
|
|
history []RequestRecord
|
|
requestHook func(command string, reqBody []byte, c *gin.Context)
|
|
}
|
|
|
|
// NewServer creates a new mock webhook server instance.
|
|
func NewServer() *Server {
|
|
engine := gin.New()
|
|
engine.Use(gin.Recovery())
|
|
|
|
s := &Server{
|
|
Engine: engine,
|
|
handlers: make(map[string]gin.HandlerFunc),
|
|
}
|
|
|
|
engine.POST("/callbackExample/:command", func(c *gin.Context) {
|
|
s.dispatch(c, c.Param("command"))
|
|
})
|
|
engine.POST("/:command", func(c *gin.Context) {
|
|
s.dispatch(c, c.Param("command"))
|
|
})
|
|
|
|
s.RegisterDefaultHandlers()
|
|
return s
|
|
}
|
|
|
|
// SetRecordHistory enables or disables recording incoming request history.
|
|
func (s *Server) SetRecordHistory(enabled bool) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.recordHistory = enabled
|
|
}
|
|
|
|
// OnRequest sets a callback function invoked whenever any webhook request arrives.
|
|
func (s *Server) OnRequest(hook func(command string, reqBody []byte, c *gin.Context)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.requestHook = hook
|
|
}
|
|
|
|
// GetHistory returns a copy of all recorded requests.
|
|
func (s *Server) GetHistory() []RequestRecord {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
result := make([]RequestRecord, len(s.history))
|
|
copy(result, s.history)
|
|
return result
|
|
}
|
|
|
|
// ClearHistory clears all recorded request history.
|
|
func (s *Server) ClearHistory() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.history = nil
|
|
}
|
|
|
|
// Start runs the webhook HTTP server on the given address (e.g. ":10006").
|
|
func (s *Server) Start(addr string) error {
|
|
s.httpServer = &http.Server{
|
|
Addr: addr,
|
|
Handler: s.Engine,
|
|
}
|
|
return s.httpServer.ListenAndServe()
|
|
}
|
|
|
|
// Shutdown gracefully stops the running HTTP server.
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
if s.httpServer == nil {
|
|
return nil
|
|
}
|
|
return s.httpServer.Shutdown(ctx)
|
|
}
|
|
|
|
func (s *Server) dispatch(c *gin.Context, command string) {
|
|
s.mu.RLock()
|
|
h, ok := s.handlers[command]
|
|
s.mu.RUnlock()
|
|
if ok {
|
|
h(c)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, cbapi.CommonCallbackResp{})
|
|
}
|
|
|
|
// ServeHTTP conforms to the http.Handler interface.
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.Engine.ServeHTTP(w, r)
|
|
}
|
|
|
|
// RegisterCallback registers a typed handler for a callback command.
|
|
func RegisterCallback[Req any, Resp any](s *Server, command string, fn func(c *gin.Context, req *Req) (*Resp, error)) {
|
|
handler := func(c *gin.Context) {
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
if s.recordHistory {
|
|
s.history = append(s.history, RequestRecord{
|
|
Timestamp: time.Now(),
|
|
Command: command,
|
|
Body: body,
|
|
Headers: c.Request.Header.Clone(),
|
|
})
|
|
}
|
|
hook := s.requestHook
|
|
s.mu.Unlock()
|
|
|
|
if hook != nil {
|
|
hook(command, body, c)
|
|
}
|
|
|
|
var req Req
|
|
if len(body) > 0 {
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
c.String(http.StatusBadRequest, fmt.Sprintf("unmarshal error: %v", err))
|
|
return
|
|
}
|
|
}
|
|
resp, err := fn(c, &req)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if resp != nil {
|
|
c.JSON(http.StatusOK, resp)
|
|
} else {
|
|
c.JSON(http.StatusOK, cbapi.CommonCallbackResp{})
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.handlers[command] = handler
|
|
s.mu.Unlock()
|
|
}
|