530 lines
20 KiB
Go
530 lines
20 KiB
Go
package v1
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/middleware"
|
|
"github.com/gochat/gochat/internal/search"
|
|
"github.com/gochat/gochat/internal/service"
|
|
"github.com/gochat/gochat/pkg/pagination"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
// MessageHandler handles message-related API endpoints.
|
|
// Reference: Chatwoot app/controllers/api/v1/messages_controller.rb
|
|
type MessageHandler struct {
|
|
svc *service.MessageService
|
|
}
|
|
|
|
// NewMessageHandler creates a new MessageHandler.
|
|
func NewMessageHandler(svc *service.MessageService) *MessageHandler {
|
|
return &MessageHandler{svc: svc}
|
|
}
|
|
|
|
// @Summary List messages in a conversation
|
|
// @Description Retrieves all messages for a conversation with pagination
|
|
// @Tags Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param conversation_id path uint true "Conversation ID"
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param page_size query int false "Items per page" default(25)
|
|
// @Success 200 {object} []model.Message
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 404 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages [get]
|
|
// List retrieves all messages for a conversation with pagination.
|
|
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages
|
|
// Reference: Chatwoot conversations#messages (index)
|
|
func (h *MessageHandler) List(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
|
return
|
|
}
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
after, _ := strconv.ParseUint(c.Query("after"), 10, 64)
|
|
before, _ := strconv.ParseUint(c.Query("before"), 10, 64)
|
|
filterInternal := c.Query("filter_internal_messages") != ""
|
|
messages, _, svcErr := h.svc.ListByConversationFinder(c.Request.Context(), conversation.ID, uint(after), uint(before), filterInternal)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeMessageIndex(c.Request.Context(), h.svc.DB(), conversation, messages))
|
|
}
|
|
|
|
// @Summary Create a message in a conversation
|
|
// @Description Creates a new message in an existing conversation
|
|
// @Tags Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param conversation_id path uint true "Conversation ID"
|
|
// @Param body body service.CreateMessageRequest true "Message creation payload"
|
|
// @Success 201 {object} model.Message
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 404 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages [post]
|
|
// Create creates a new message in a conversation.
|
|
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages
|
|
// Reference: Chatwoot conversations#messages (create)
|
|
func (h *MessageHandler) Create(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
connectorRequest := middleware.IsConnectorService(c)
|
|
userID := getUserID(c)
|
|
if userID == 0 && !connectorRequest {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
|
|
return
|
|
}
|
|
|
|
var req service.CreateMessageRequest
|
|
if err := bindCreateMessageRequest(c, &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
conversationID, urlErr := parseUintParam(c, "conversation_id")
|
|
if urlErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_id is required")
|
|
return
|
|
}
|
|
conversation, resolveErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if resolveErr != nil {
|
|
handleServiceError(c, resolveErr)
|
|
return
|
|
}
|
|
req.ConversationID = conversation.ID
|
|
if !requireConnectorShangwutongConversation(c, h.svc.DB(), conversation) {
|
|
return
|
|
}
|
|
if connectorRequest {
|
|
if !validateConnectorMessageImport(c, &req, conversation.InboxID) {
|
|
return
|
|
}
|
|
} else if req.External {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "external messages require connector authentication")
|
|
return
|
|
}
|
|
|
|
message, svcErr := h.svc.Create(c.Request.Context(), accountID, userID, req)
|
|
if svcErr != nil {
|
|
if errors.Is(svcErr, service.ErrMessageIdempotencyConflict) {
|
|
connectorRouteError(c, http.StatusConflict, "idempotency_conflict", svcErr.Error())
|
|
return
|
|
}
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
|
}
|
|
|
|
// @Summary Get a single message
|
|
// @Description Retrieves detailed information about a specific message
|
|
// @Tags Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param conversation_id path uint true "Conversation ID"
|
|
// @Param id path uint true "Message ID"
|
|
// @Success 200 {object} model.Message
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 404 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id} [get]
|
|
// Get retrieves a single message.
|
|
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
|
|
// Reference: Chatwoot messages#show
|
|
func (h *MessageHandler) Get(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
messageID, err := parseUintParam(c, "message_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
|
return
|
|
}
|
|
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
message, svcErr := h.svc.GetByAccountConversationAndID(c.Request.Context(), accountID, conversation.ID, messageID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
|
}
|
|
|
|
// Update updates a message's content.
|
|
// PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
|
|
func (h *MessageHandler) Update(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
messageID, err := parseUintParam(c, "message_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateMessageRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
message, svcErr := h.svc.UpdateInConversation(c.Request.Context(), accountID, conversation.ID, messageID, req)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
|
}
|
|
|
|
// @Summary Delete a message
|
|
// @Description Soft-deletes a message from a conversation
|
|
// @Tags Messages
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param account_id path uint true "Account ID"
|
|
// @Param conversation_id path uint true "Conversation ID"
|
|
// @Param id path uint true "Message ID"
|
|
// @Success 204 {object} object
|
|
// @Failure 400 {object} model.ErrorResponse
|
|
// @Failure 401 {object} model.ErrorResponse
|
|
// @Failure 404 {object} model.ErrorResponse
|
|
// @Failure 500 {object} model.ErrorResponse
|
|
// @Security ApiKeyAuth
|
|
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id} [delete]
|
|
// Delete soft-deletes a message.
|
|
// DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
|
|
func (h *MessageHandler) Delete(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
messageID, err := parseUintParam(c, "message_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
|
return
|
|
}
|
|
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if !requireConnectorShangwutongConversation(c, h.svc.DB(), conversation) {
|
|
return
|
|
}
|
|
message, svcErr := h.svc.DeleteInConversation(c.Request.Context(), accountID, conversation.ID, messageID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
if middleware.IsConnectorService(c) {
|
|
c.Status(http.StatusNoContent)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
|
}
|
|
|
|
// Search searches messages by content within an account.
|
|
// GET /api/v1/accounts/:account_id/messages/search?q=...
|
|
// Reference: Chatwoot messages#search
|
|
func (h *MessageHandler) Search(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
q := c.Query("q")
|
|
if q == "" {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "search query 'q' is required")
|
|
return
|
|
}
|
|
|
|
p := pagination.Parse(c)
|
|
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
|
|
|
|
messages, total, svcErr := h.svc.Search(c.Request.Context(), accountID, q, p.Offset, p.PerPage, searchMode)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, toInterfaceSlice(messages), p.Page, p.PerPage, total)
|
|
}
|
|
|
|
// Retry retries a failed message.
|
|
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id/retry
|
|
func (h *MessageHandler) Retry(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
messageID, err := parseUintParam(c, "message_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
|
return
|
|
}
|
|
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, svcErr.Error())
|
|
return
|
|
}
|
|
message, svcErr := h.svc.RetryInConversation(c.Request.Context(), accountID, conversation.ID, messageID)
|
|
if svcErr != nil {
|
|
// Chatwoot retry rescues creation/status-update errors through render_could_not_create_error,
|
|
// so a missing message on this action returns 422 instead of the generic show/delete 404.
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, svcErr.Error())
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, serializeMessage(c.Request.Context(), h.svc.DB(), message, conversation))
|
|
}
|
|
|
|
// Translate translates a message's content to a target language using LLM.
|
|
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id/translate
|
|
// Reference: Chatwoot messages#translate
|
|
func (h *MessageHandler) Translate(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
messageID, err := parseUintParam(c, "message_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
|
|
return
|
|
}
|
|
conversationID, err := parseUintParam(c, "conversation_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id")
|
|
return
|
|
}
|
|
|
|
var req service.TranslateMessageRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
conversation, svcErr := h.svc.ResolveConversationForRoute(c.Request.Context(), accountID, conversationID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
result, svcErr := h.svc.TranslateInConversation(c.Request.Context(), accountID, conversation.ID, messageID, req)
|
|
if svcErr != nil {
|
|
lower := strings.ToLower(svcErr.Error())
|
|
if strings.Contains(lower, "not found") {
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, svcErr.Error())
|
|
return
|
|
}
|
|
if strings.Contains(lower, "required") || strings.Contains(lower, "invalid") {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, svcErr.Error())
|
|
return
|
|
}
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
|
|
return
|
|
}
|
|
if result.AlreadyTranslated {
|
|
c.Status(http.StatusOK)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"content": result.TranslatedContent})
|
|
}
|
|
|
|
func bindCreateMessageRequest(c *gin.Context, req *service.CreateMessageRequest) error {
|
|
if strings.HasPrefix(c.ContentType(), "multipart/form-data") {
|
|
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
|
return err
|
|
}
|
|
req.Content = c.PostForm("content")
|
|
req.MessageType = c.PostForm("message_type")
|
|
req.ContentType = c.PostForm("content_type")
|
|
if senderID, err := strconv.ParseUint(c.PostForm("sender_id"), 10, 64); err == nil {
|
|
req.SenderID = uint(senderID)
|
|
}
|
|
req.SenderType = c.PostForm("sender_type")
|
|
req.SourceID = c.PostForm("source_id")
|
|
req.EchoID = c.PostForm("echo_id")
|
|
req.ExternalCreatedAt = c.PostForm("external_created_at")
|
|
req.External = strings.EqualFold(c.PostForm("external"), "true") || c.PostForm("external") == "1"
|
|
req.EmailHTMLContent = c.PostForm("email_html_content")
|
|
req.CCEmails = c.PostForm("cc_emails")
|
|
req.BCCEmails = c.PostForm("bcc_emails")
|
|
req.ToEmails = c.PostForm("to_emails")
|
|
req.Private = strings.EqualFold(c.PostForm("private"), "true") || c.PostForm("private") == "1"
|
|
req.IsVoiceMessage = strings.EqualFold(c.PostForm("is_voice_message"), "true") || c.PostForm("is_voice_message") == "1"
|
|
req.CampaignID = c.PostForm("campaign_id")
|
|
if raw := c.PostForm("content_attributes"); raw != "" {
|
|
req.ContentAttributes = []byte(raw)
|
|
}
|
|
if raw := c.PostForm("additional_attributes"); raw != "" {
|
|
req.AdditionalAttributes = []byte(raw)
|
|
}
|
|
if raw := c.PostForm("external_source_ids"); raw != "" {
|
|
req.ExternalSourceIDs = []byte(raw)
|
|
}
|
|
if raw := c.PostForm("template_params"); raw != "" {
|
|
req.TemplateParams = []byte(raw)
|
|
}
|
|
if c.Request.MultipartForm != nil {
|
|
for _, key := range []string{"attachments[]", "attachments"} {
|
|
for _, file := range c.Request.MultipartForm.File[key] {
|
|
digest := ""
|
|
if opened, err := file.Open(); err == nil {
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, opened); err == nil {
|
|
digest = fmt.Sprintf("%x", hash.Sum(nil))
|
|
}
|
|
_ = opened.Close()
|
|
}
|
|
req.Attachments = append(req.Attachments, service.MessageAttachmentInput{
|
|
FileName: file.Filename,
|
|
FileSize: int(file.Size),
|
|
ContentType: file.Header.Get("Content-Type"),
|
|
SHA256: digest,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var raw map[string]json.RawMessage
|
|
if err := c.ShouldBindJSON(&raw); err != nil {
|
|
return err
|
|
}
|
|
bytes, _ := json.Marshal(raw)
|
|
if err := json.Unmarshal(bytes, req); err != nil {
|
|
return err
|
|
}
|
|
if value, ok := raw["content_attributes"]; ok && string(value) != "null" {
|
|
req.ContentAttributes = datatypes.JSON(value)
|
|
}
|
|
if value, ok := raw["additional_attributes"]; ok && string(value) != "null" {
|
|
req.AdditionalAttributes = datatypes.JSON(value)
|
|
}
|
|
if value, ok := raw["external_source_ids"]; ok && string(value) != "null" {
|
|
req.ExternalSourceIDs = datatypes.JSON(value)
|
|
}
|
|
if value, ok := raw["template_params"]; ok && string(value) != "null" {
|
|
req.TemplateParams = datatypes.JSON(value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateConnectorMessageImport(c *gin.Context, req *service.CreateMessageRequest, inboxID uint) bool {
|
|
messageType := strings.ToLower(strings.TrimSpace(req.MessageType))
|
|
if !req.External || req.Private || (messageType != "incoming" && messageType != "outgoing" && messageType != "activity") {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_message_import", "connector messages must be external, non-private incoming, outgoing, or activity messages")
|
|
return false
|
|
}
|
|
wantPrefix := fmt.Sprintf("swt:%d:", inboxID)
|
|
if !strings.HasPrefix(req.SourceID, wantPrefix) || c.GetHeader("Idempotency-Key") != req.SourceID {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_idempotency_key", "source_id and Idempotency-Key must match the shangwutong inbox namespace")
|
|
return false
|
|
}
|
|
if strings.TrimSpace(req.ExternalCreatedAt) == "" {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_external_created_at", "external_created_at is required")
|
|
return false
|
|
}
|
|
if _, err := time.Parse(time.RFC3339Nano, req.ExternalCreatedAt); err != nil {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_external_created_at", "external_created_at must use RFC3339Nano")
|
|
return false
|
|
}
|
|
if len(req.ExternalSourceIDs) > 0 && string(req.ExternalSourceIDs) != "null" {
|
|
ids := map[string]any{}
|
|
if json.Unmarshal(req.ExternalSourceIDs, &ids) != nil || len(ids) > 1 {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_external_source_ids", "external_source_ids is invalid")
|
|
return false
|
|
}
|
|
if value, exists := ids["shangwutong"]; exists {
|
|
text, ok := value.(string)
|
|
if !ok || strings.TrimSpace(text) == "" {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_external_source_ids", "shangwutong external message ID must be a decimal string")
|
|
return false
|
|
}
|
|
if _, err := strconv.ParseUint(text, 10, 64); err != nil {
|
|
connectorRouteError(c, http.StatusUnprocessableEntity, "invalid_external_source_ids", "shangwutong external message ID must be a decimal string")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|