feat(channels): add Shangwutong connector
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
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"
|
||||
@@ -99,8 +105,9 @@ func (h *MessageHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
connectorRequest := middleware.IsConnectorService(c)
|
||||
userID := getUserID(c)
|
||||
if userID == 0 {
|
||||
if userID == 0 && !connectorRequest {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
|
||||
return
|
||||
}
|
||||
@@ -122,9 +129,24 @@ func (h *MessageHandler) Create(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
@@ -256,11 +278,18 @@ func (h *MessageHandler) Delete(c *gin.Context) {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -393,6 +422,7 @@ func bindCreateMessageRequest(c *gin.Context, req *service.CreateMessageRequest)
|
||||
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")
|
||||
@@ -403,16 +433,31 @@ func bindCreateMessageRequest(c *gin.Context, req *service.CreateMessageRequest)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -431,8 +476,54 @@ func bindCreateMessageRequest(c *gin.Context, req *service.CreateMessageRequest)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user