116 lines
2.7 KiB
Go
116 lines
2.7 KiB
Go
|
|
package milky
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
|
||
|
|
"github.com/bytedance/sonic"
|
||
|
|
"github.com/valyala/fasthttp"
|
||
|
|
"go.uber.org/zap"
|
||
|
|
)
|
||
|
|
|
||
|
|
// WebhookServer Webhook 服务器
|
||
|
|
// 用于接收协议端 POST 推送的事件
|
||
|
|
type WebhookServer struct {
|
||
|
|
server *fasthttp.Server
|
||
|
|
eventChan chan []byte
|
||
|
|
logger *zap.Logger
|
||
|
|
addr string
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewWebhookServer 创建 Webhook 服务器
|
||
|
|
func NewWebhookServer(addr string, logger *zap.Logger) *WebhookServer {
|
||
|
|
return &WebhookServer{
|
||
|
|
eventChan: make(chan []byte, 100),
|
||
|
|
logger: logger.Named("webhook-server"),
|
||
|
|
addr: addr,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start 启动服务器
|
||
|
|
func (s *WebhookServer) Start() error {
|
||
|
|
s.server = &fasthttp.Server{
|
||
|
|
Handler: s.handleRequest,
|
||
|
|
MaxConnsPerIP: 1000,
|
||
|
|
MaxRequestsPerConn: 1000,
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("Starting webhook server", zap.String("addr", s.addr))
|
||
|
|
|
||
|
|
go func() {
|
||
|
|
if err := s.server.ListenAndServe(s.addr); err != nil {
|
||
|
|
s.logger.Error("Webhook server error", zap.Error(err))
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleRequest 处理请求
|
||
|
|
func (s *WebhookServer) handleRequest(ctx *fasthttp.RequestCtx) {
|
||
|
|
// 只接受 POST 请求
|
||
|
|
if !ctx.IsPost() {
|
||
|
|
s.logger.Warn("Received non-POST request",
|
||
|
|
zap.String("method", string(ctx.Method())))
|
||
|
|
ctx.Error("Method Not Allowed", fasthttp.StatusMethodNotAllowed)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查 Content-Type
|
||
|
|
contentType := string(ctx.Request.Header.ContentType())
|
||
|
|
if contentType != "application/json" {
|
||
|
|
s.logger.Warn("Invalid content type",
|
||
|
|
zap.String("content_type", contentType))
|
||
|
|
ctx.Error("Unsupported Media Type", fasthttp.StatusUnsupportedMediaType)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取请求体
|
||
|
|
body := ctx.PostBody()
|
||
|
|
if len(body) == 0 {
|
||
|
|
s.logger.Warn("Empty request body")
|
||
|
|
ctx.Error("Bad Request", fasthttp.StatusBadRequest)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// 验证 JSON 格式
|
||
|
|
var event Event
|
||
|
|
if err := sonic.Unmarshal(body, &event); err != nil {
|
||
|
|
s.logger.Error("Failed to parse event", zap.Error(err))
|
||
|
|
ctx.Error("Bad Request", fasthttp.StatusBadRequest)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Debug("Received webhook event",
|
||
|
|
zap.String("event_type", event.EventType),
|
||
|
|
zap.Int64("self_id", event.SelfID))
|
||
|
|
|
||
|
|
// 发送到事件通道
|
||
|
|
select {
|
||
|
|
case s.eventChan <- body:
|
||
|
|
default:
|
||
|
|
s.logger.Warn("Event channel full, dropping event")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 返回成功响应
|
||
|
|
ctx.SetContentType("application/json")
|
||
|
|
ctx.SetStatusCode(fasthttp.StatusOK)
|
||
|
|
ctx.SetBodyString(`{"status":"ok"}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Events 获取事件通道
|
||
|
|
func (s *WebhookServer) Events() <-chan []byte {
|
||
|
|
return s.eventChan
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stop 停止服务器
|
||
|
|
func (s *WebhookServer) Stop() error {
|
||
|
|
if s.server != nil {
|
||
|
|
s.logger.Info("Stopping webhook server")
|
||
|
|
if err := s.server.Shutdown(); err != nil {
|
||
|
|
return fmt.Errorf("failed to shutdown webhook server: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
close(s.eventChan)
|
||
|
|
return nil
|
||
|
|
}
|