package v1 import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/middleware" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/ws" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/gorm" ) func publishRealtimeEvent(publisher *ws.EventPublisher, accountID uint, eventType string, payload any) { if publisher == nil { return } if err := publisher.PublishEvent(accountID, eventType, payload); err != nil { applogger.L().Errorf("publish realtime event %s for account %d: %v", eventType, accountID, err) } } func bindJSONWrappedOrRaw(c *gin.Context, wrapperKey string, target any) error { if c.Request.Body == nil { return fmt.Errorf("empty request body") } body, err := io.ReadAll(c.Request.Body) if err != nil { return err } c.Request.Body = io.NopCloser(bytes.NewReader(body)) if len(bytes.TrimSpace(body)) == 0 { return fmt.Errorf("empty request body") } var wrapper map[string]json.RawMessage if err := json.Unmarshal(body, &wrapper); err == nil { if raw, ok := wrapper[wrapperKey]; ok && len(raw) > 0 && string(raw) != "null" { return json.Unmarshal(raw, target) } } return json.Unmarshal(body, target) } // parseUintParam extracts a uint path parameter from the Gin context. func parseUintParam(c *gin.Context, param string) (uint, error) { val := c.Param(param) n, err := strconv.ParseUint(val, 10, 32) if err != nil { return 0, err } return uint(n), nil } func parseOptionalUintQueryParam(c *gin.Context, param string) (uint, error) { val := c.Query(param) if val == "" { return 0, nil } n, err := strconv.ParseUint(val, 10, 32) if err != nil { return 0, err } return uint(n), nil } func parseIntQueryDefault(c *gin.Context, param string, fallback int) (int, error) { val := c.Query(param) if val == "" { return fallback, nil } n, err := strconv.Atoi(val) if err != nil { return fallback, err } return n, nil } func fixedPageOffset(c *gin.Context, perPage int) (int, int) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 } return page, (page - 1) * perPage } func parseUintAnyParam(c *gin.Context, params ...string) (uint, error) { var lastErr error for _, param := range params { if c.Param(param) == "" { continue } id, err := parseUintParam(c, param) if err == nil && id != 0 { return id, nil } return 0, err } return 0, lastErr } // parseAccountIDParam accepts both Chatwoot-style :account_id and older local // tests/routes that still mount the account parameter as :id. func parseAccountIDParam(c *gin.Context) uint { if id, err := parseUintParam(c, "account_id"); err == nil && id != 0 { return id } if id, err := parseUintParam(c, "id"); err == nil && id != 0 { return id } return 0 } // getAccountID extracts account ID from the request. // Priority: URL param account_id > X-Account-ID header > JWT claims account_id func getAccountID(c *gin.Context) uint { // Try URL param — Chatwoot uses :account_id, GoChat routes use :account_id where possible if id, err := parseUintParam(c, "account_id"); err == nil && id != 0 { return id } // Try X-Account-ID header headerAccountID := c.GetHeader("X-Account-ID") if headerAccountID != "" { id, err := strconv.ParseUint(headerAccountID, 10, 32) if err == nil && id != 0 { return uint(id) } } // Fall back to JWT claims (set by AuthRequired middleware) if accountID, exists := c.Get("account_id"); exists { switch v := accountID.(type) { case uint: if v != 0 { return v } case float64: if v != 0 { return uint(v) } case int: if v != 0 { return uint(v) } } } return 0 } // getRole extracts the current user's role from the Gin context. // Returns the role string (e.g. "administrator", "agent") from auth middleware claims. func getRole(c *gin.Context) string { if role, exists := c.Get("role"); exists { if s, ok := role.(string); ok { return s } } return "" } // getCustomRoleID extracts the current user's custom role ID from the Gin context. func getCustomRoleID(c *gin.Context) uint { if id, exists := c.Get("custom_role_id"); exists { switch v := id.(type) { case uint: return v case float64: return uint(v) case int: return uint(v) } } return 0 } // getUserID extracts the current user ID from the Gin context. // Priority: JWT claims > X-User-ID header func getUserID(c *gin.Context) uint { if userID, exists := c.Get("user_id"); exists { switch v := userID.(type) { case uint: return v case float64: return uint(v) case int: return uint(v) case string: n, err := strconv.ParseUint(v, 10, 32) if err == nil { return uint(n) } } } // Fallback: X-User-ID header headerUserID := c.GetHeader("X-User-ID") if headerUserID != "" { n, err := strconv.ParseUint(headerUserID, 10, 32) if err == nil { return uint(n) } } return 0 } // getPage extracts page number from query params (default: 1). func getPage(c *gin.Context) int { val := c.DefaultQuery("page", "1") n, err := strconv.Atoi(val) if err != nil || n < 1 { return 1 } return n } // getPageSize extracts page size from query params (default: 25, max: 100). func getPageSize(c *gin.Context) int { val := c.DefaultQuery("page_size", "25") n, err := strconv.Atoi(val) if err != nil || n < 1 { return 25 } if n > 100 { return 100 } return n } func requireConnectorShangwutongConversation(c *gin.Context, db *gorm.DB, conversation *model.Conversation) bool { if !middleware.IsConnectorService(c) { return true } if c.GetHeader("X-GoChat-Schema-Version") != "1" || db == nil || conversation == nil { connectorRouteError(c, http.StatusUnprocessableEntity, "unsupported_schema_version", "X-GoChat-Schema-Version must be 1") return false } var count int64 if err := db.WithContext(c.Request.Context()).Model(&model.Inbox{}).Where( "id = ? AND account_id = ? AND channel_type = ?", conversation.InboxID, conversation.AccountID, "shangwutong", ).Count(&count).Error; err != nil || count != 1 { connectorRouteError(c, http.StatusForbidden, "forbidden", "connector cannot access this conversation") return false } return true } func connectorRouteError(c *gin.Context, status int, code, message string) { c.AbortWithStatusJSON(status, gin.H{"error": gin.H{ "code": code, "message": message, "retryable": false, "request_id": c.GetString("request_id"), }}) }