|
|
|
@@ -15,12 +15,15 @@ import (
|
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
|
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
|
|
|
"github.com/gochat/gochat/internal/service"
|
|
|
|
|
"github.com/gochat/gochat/internal/worker"
|
|
|
|
|
"gorm.io/datatypes"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ShangwutongConnectorHandler struct {
|
|
|
|
|
db *gorm.DB
|
|
|
|
|
messageSvc *service.MessageService
|
|
|
|
|
worker *worker.WorkerPool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongContactMetadataRequest struct {
|
|
|
|
@@ -75,8 +78,12 @@ func (h *ShangwutongConnectorHandler) UpdateContactMetadata(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updated": true})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewShangwutongConnectorHandler(db *gorm.DB, messageSvc *service.MessageService) *ShangwutongConnectorHandler {
|
|
|
|
|
return &ShangwutongConnectorHandler{db: db, messageSvc: messageSvc}
|
|
|
|
|
func NewShangwutongConnectorHandler(db *gorm.DB, messageSvc *service.MessageService, pools ...*worker.WorkerPool) *ShangwutongConnectorHandler {
|
|
|
|
|
var pool *worker.WorkerPool
|
|
|
|
|
if len(pools) > 0 {
|
|
|
|
|
pool = pools[0]
|
|
|
|
|
}
|
|
|
|
|
return &ShangwutongConnectorHandler{db: db, messageSvc: messageSvc, worker: pool}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongConnectorInbox struct {
|
|
|
|
@@ -270,6 +277,454 @@ func (h *ShangwutongConnectorHandler) UpdateMessageStatus(c *gin.Context) {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongClassificationCacheResponse struct {
|
|
|
|
|
InboxID uint `json:"inbox_id"`
|
|
|
|
|
ConversationKinds json.RawMessage `json:"conversation_kinds"`
|
|
|
|
|
CustomerColorKinds json.RawMessage `json:"customer_color_kinds"`
|
|
|
|
|
SyncStatus string `json:"sync_status"`
|
|
|
|
|
SyncedAt *time.Time `json:"synced_at,omitempty"`
|
|
|
|
|
LastErrorCode *string `json:"last_error_code,omitempty"`
|
|
|
|
|
LastErrorMessage *string `json:"last_error_message,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongClassificationCallbackRequest struct {
|
|
|
|
|
EventID string `json:"event_id"`
|
|
|
|
|
ConversationKinds []shangwutongConversationKind `json:"conversation_kinds"`
|
|
|
|
|
CustomerColorKinds []shangwutongCustomerColorKind `json:"customer_colors"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongConversationKind struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
IconIndex int `json:"icon_index"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongCustomerColorKind struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) ListClassifications(c *gin.Context) {
|
|
|
|
|
inbox, ok := h.userInbox(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var cache model.ShangwutongClassificationCache
|
|
|
|
|
err := h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&cache).Error
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
c.JSON(http.StatusOK, shangwutongClassificationCacheResponse{
|
|
|
|
|
InboxID: inbox.ID, ConversationKinds: json.RawMessage(`[]`), CustomerColorKinds: json.RawMessage(`[]`), SyncStatus: "never",
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_lookup_failed", "failed to load classifications", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, classificationCacheResponse(&cache))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) SyncClassifications(c *gin.Context) {
|
|
|
|
|
inbox, ok := h.userInbox(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if h.worker == nil {
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, "classification_sync_unavailable", "classification sync is unavailable", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pending := map[string]any{"sync_status": "pending", "last_error_code": nil, "last_error_message": nil}
|
|
|
|
|
var cache model.ShangwutongClassificationCache
|
|
|
|
|
err := h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&cache).Error
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
|
|
|
|
cache = model.ShangwutongClassificationCache{InboxID: inbox.ID, ConversationKinds: datatypes.JSON([]byte(`[]`)), CustomerColorKinds: datatypes.JSON([]byte(`[]`)), SyncStatus: "pending"}
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Create(&cache).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_sync_state_failed", "failed to create sync state", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
case err != nil:
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_sync_state_failed", "failed to load sync state", true)
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Model(&cache).Updates(pending).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_sync_state_failed", "failed to update sync state", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
eventID, err := service.EnqueueShangwutongClassificationSync(c.Request.Context(), h.worker, inbox)
|
|
|
|
|
if err != nil {
|
|
|
|
|
code, message := "classification_sync_queue_failed", err.Error()
|
|
|
|
|
_ = h.db.WithContext(c.Request.Context()).Model(&cache).Updates(map[string]any{"sync_status": "failed", "last_error_code": code, "last_error_message": message})
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, code, "failed to queue classification sync", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"sync_id": eventID, "sync_status": "pending"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongConversationClassificationRequest struct {
|
|
|
|
|
ChatKindID string `json:"chat_kind_id"`
|
|
|
|
|
CustomerColorID string `json:"customer_color_id"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) UpdateConversationClassification(c *gin.Context) {
|
|
|
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
|
|
|
if err != nil || accountID == 0 {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "conversation not found", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conversationID, err := strconv.ParseUint(c.Param("conversation_id"), 10, 64)
|
|
|
|
|
if err != nil || conversationID == 0 {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "conversation not found", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var request shangwutongConversationClassificationRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classification_change", "classification change payload is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
request.ChatKindID, request.CustomerColorID = strings.TrimSpace(request.ChatKindID), strings.TrimSpace(request.CustomerColorID)
|
|
|
|
|
if (request.ChatKindID == "") == (request.CustomerColorID == "") {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classification_change", "exactly one classification is required", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var conversation model.Conversation
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND account_id = ?", uint(conversationID), uint(accountID)).First(&conversation).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "conversation not found", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var inbox model.Inbox
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND account_id = ? AND channel_type = ?", conversation.InboxID, uint(accountID), "shangwutong").First(&inbox).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "not_shangwutong_conversation", "conversation is not a Shangwutong conversation", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var contactInbox model.ContactInbox
|
|
|
|
|
query := h.db.WithContext(c.Request.Context()).Where("inbox_id = ? AND contact_id = ?", inbox.ID, conversation.ContactID)
|
|
|
|
|
if conversation.ContactInboxID != nil && *conversation.ContactInboxID != 0 {
|
|
|
|
|
query = h.db.WithContext(c.Request.Context()).Where("id = ? AND inbox_id = ?", *conversation.ContactInboxID, inbox.ID)
|
|
|
|
|
}
|
|
|
|
|
if err := query.First(&contactInbox).Error; err != nil || strings.TrimSpace(contactInbox.SourceID) == "" {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "conversation_target_unavailable", "conversation has no Shangwutong session", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var cache model.ShangwutongClassificationCache
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&cache).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusConflict, "classification_not_synced", "sync Shangwutong classifications first", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if cache.SyncStatus != "succeeded" {
|
|
|
|
|
h.connectorError(c, http.StatusConflict, "classification_not_synced", "sync Shangwutong classifications first", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var conversationKinds []shangwutongConversationKind
|
|
|
|
|
var customerColors []shangwutongCustomerColorKind
|
|
|
|
|
if err := json.Unmarshal(cache.ConversationKinds, &conversationKinds); err != nil || json.Unmarshal(cache.CustomerColorKinds, &customerColors) != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_invalid", "stored classifications are invalid", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
colorName := ""
|
|
|
|
|
if request.ChatKindID != "" {
|
|
|
|
|
if !containsConversationKind(conversationKinds, request.ChatKindID) {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "classification_not_found", "conversation classification is not in the synced catalog", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for _, color := range customerColors {
|
|
|
|
|
if color.ID == request.CustomerColorID {
|
|
|
|
|
colorName = color.Name
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if colorName == "" {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "classification_not_found", "customer classification is not in the synced catalog", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var metadata struct {
|
|
|
|
|
CID string `json:"cid"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(contactInbox.ChannelMetadata, &metadata); err != nil || strings.TrimSpace(metadata.CID) == "" {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "conversation_target_unavailable", "conversation has no customer cid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
request.CustomerColorID = strings.TrimSpace(request.CustomerColorID)
|
|
|
|
|
if h.worker == nil {
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, "classification_queue_unavailable", "classification queue is unavailable", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eventID, err := service.EnqueueShangwutongClassificationChange(c.Request.Context(), h.worker, &inbox, conversation.ID, contactInbox.SourceID, metadata.CID, "", request.CustomerColorID, colorName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, "classification_queue_failed", "failed to queue classification change", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"sync_id": eventID, "status": "pending", "customer_color_id": request.CustomerColorID})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if h.worker == nil {
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, "classification_queue_unavailable", "classification queue is unavailable", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
eventID, err := service.EnqueueShangwutongClassificationChange(c.Request.Context(), h.worker, &inbox, conversation.ID, contactInbox.SourceID, "", request.ChatKindID, "", "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusServiceUnavailable, "classification_queue_failed", "failed to queue classification change", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"sync_id": eventID, "status": "pending", "chat_kind_id": request.ChatKindID})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsConversationKind(kinds []shangwutongConversationKind, id string) bool {
|
|
|
|
|
for _, kind := range kinds {
|
|
|
|
|
if kind.ID == id {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) UpdateClassificationCatalog(c *gin.Context) {
|
|
|
|
|
inbox, ok := h.authorizedInbox(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var request shangwutongClassificationCallbackRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.EventID) == "" || len(request.EventID) > 128 {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classifications", "classification payload is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
expectedKey := fmt.Sprintf("swt-classification-sync:%d:%s", inbox.ID, request.EventID)
|
|
|
|
|
if c.GetHeader("Idempotency-Key") != expectedKey {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_idempotency_key", "Idempotency-Key does not match event_id", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !validClassificationCatalog(request) {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classifications", "classification payload is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conversationKinds, err := json.Marshal(request.ConversationKinds)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classifications", "classification payload is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
customerColors, err := json.Marshal(request.CustomerColorKinds)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classifications", "classification payload is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var cache model.ShangwutongClassificationCache
|
|
|
|
|
err = h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&cache).Error
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
cache = model.ShangwutongClassificationCache{InboxID: inbox.ID}
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_update_failed", "failed to load classification cache", true)
|
|
|
|
|
return
|
|
|
|
|
} else if cache.LastSyncEventID == request.EventID {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updated": false, "sync_status": cache.SyncStatus})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
cache.ConversationKinds = conversationKinds
|
|
|
|
|
cache.CustomerColorKinds = customerColors
|
|
|
|
|
cache.SyncStatus = "succeeded"
|
|
|
|
|
cache.SyncedAt = &now
|
|
|
|
|
cache.LastErrorCode = nil
|
|
|
|
|
cache.LastErrorMessage = nil
|
|
|
|
|
cache.LastSyncEventID = request.EventID
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Save(&cache).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_update_failed", "failed to save classifications", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updated": true, "sync_status": cache.SyncStatus, "synced_at": cache.SyncedAt})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongClassificationSyncStatusRequest struct {
|
|
|
|
|
EventID string `json:"event_id"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
ErrorCode string `json:"error_code"`
|
|
|
|
|
ErrorMessage string `json:"error_message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) UpdateClassificationSyncStatus(c *gin.Context) {
|
|
|
|
|
inbox, ok := h.authorizedInbox(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var request shangwutongClassificationSyncStatusRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.EventID) == "" || request.Status != "failed" {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classification_sync_status", "classification sync status is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
expectedKey := fmt.Sprintf("swt-classification-sync:%d:%s", inbox.ID, request.EventID)
|
|
|
|
|
if c.GetHeader("Idempotency-Key") != expectedKey {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_idempotency_key", "Idempotency-Key does not match event_id", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var cache model.ShangwutongClassificationCache
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&cache).Error; err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
cache = model.ShangwutongClassificationCache{InboxID: inbox.ID, ConversationKinds: datatypes.JSON([]byte(`[]`)), CustomerColorKinds: datatypes.JSON([]byte(`[]`))}
|
|
|
|
|
} else {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_update_failed", "failed to load classification cache", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cache.SyncStatus = request.Status
|
|
|
|
|
cache.LastErrorCode = stringPointer(request.ErrorCode)
|
|
|
|
|
cache.LastErrorMessage = stringPointer(request.ErrorMessage)
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Save(&cache).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "classification_cache_update_failed", "failed to save classification sync status", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updated": true, "sync_status": cache.SyncStatus})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stringPointer(value string) *string {
|
|
|
|
|
value = strings.TrimSpace(value)
|
|
|
|
|
if value == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shangwutongClassificationStatusRequest struct {
|
|
|
|
|
EventID string `json:"event_id"`
|
|
|
|
|
Operation string `json:"operation"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
ChatKindID string `json:"chat_kind_id"`
|
|
|
|
|
CustomerColorID string `json:"customer_color_id"`
|
|
|
|
|
ErrorCode string `json:"error_code"`
|
|
|
|
|
ErrorMessage string `json:"error_message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) UpdateClassificationStatus(c *gin.Context) {
|
|
|
|
|
inbox, ok := h.authorizedInbox(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conversationID, err := strconv.ParseUint(c.Params.ByName("conversation_id"), 10, 64)
|
|
|
|
|
if err != nil || conversationID == 0 {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "conversation not found", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var request shangwutongClassificationStatusRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.EventID) == "" || request.Operation == "" || (request.Status != "succeeded" && request.Status != "failed" && request.Status != "uncertain") {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classification_status", "classification status is invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
expectedKey := fmt.Sprintf("swt-classification-operation:%d:%s", inbox.ID, request.EventID)
|
|
|
|
|
if c.GetHeader("Idempotency-Key") != expectedKey {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_idempotency_key", "Idempotency-Key does not match event_id", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
request.Operation = strings.TrimSpace(request.Operation)
|
|
|
|
|
request.ChatKindID, request.CustomerColorID = strings.TrimSpace(request.ChatKindID), strings.TrimSpace(request.CustomerColorID)
|
|
|
|
|
if request.Operation != "set_chat_kind" && request.Operation != "set_customer_color" || (request.ChatKindID == "") == (request.CustomerColorID == "") {
|
|
|
|
|
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_classification_status", "classification operation and target are invalid", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var conversation model.Conversation
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND inbox_id = ?", uint(conversationID), inbox.ID).First(&conversation).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "conversation not found", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
attributes := map[string]any{}
|
|
|
|
|
if len(conversation.AdditionalAttributes) > 0 {
|
|
|
|
|
if err := json.Unmarshal(conversation.AdditionalAttributes, &attributes); err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "conversation_attributes_invalid", "conversation attributes are invalid", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if request.Status == "succeeded" {
|
|
|
|
|
if request.ChatKindID != "" {
|
|
|
|
|
attributes["swt_chat_kind"] = request.ChatKindID
|
|
|
|
|
} else {
|
|
|
|
|
attributes["swt_label_color"] = request.CustomerColorID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
attributes["swt_classification_status"] = request.Status
|
|
|
|
|
attributes["swt_classification_event_id"] = request.EventID
|
|
|
|
|
if request.ErrorCode != "" {
|
|
|
|
|
attributes["swt_classification_error_code"] = request.ErrorCode
|
|
|
|
|
} else {
|
|
|
|
|
delete(attributes, "swt_classification_error_code")
|
|
|
|
|
}
|
|
|
|
|
if request.ErrorMessage != "" {
|
|
|
|
|
attributes["swt_classification_error"] = request.ErrorMessage
|
|
|
|
|
} else {
|
|
|
|
|
delete(attributes, "swt_classification_error")
|
|
|
|
|
}
|
|
|
|
|
updatedAttributes, err := json.Marshal(attributes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "conversation_attributes_invalid", "conversation attributes are invalid", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Model(&conversation).Update("additional_attributes", updatedAttributes).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusInternalServerError, "conversation_update_failed", "failed to update conversation classification", true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updated": true, "status": request.Status})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) userInbox(c *gin.Context) (*model.Inbox, bool) {
|
|
|
|
|
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
|
|
|
|
if err != nil || accountID == 0 {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "inbox not found", false)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
inboxID, err := strconv.ParseUint(c.Param("inbox_id"), 10, 64)
|
|
|
|
|
if err != nil || inboxID == 0 {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "inbox not found", false)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
var inbox model.Inbox
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("id = ? AND account_id = ? AND channel_type = ?", uint(inboxID), uint(accountID), "shangwutong").First(&inbox).Error; err != nil {
|
|
|
|
|
h.connectorError(c, http.StatusNotFound, "not_found", "inbox not found", false)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return &inbox, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func classificationCacheResponse(cache *model.ShangwutongClassificationCache) shangwutongClassificationCacheResponse {
|
|
|
|
|
conversationKinds := json.RawMessage(cache.ConversationKinds)
|
|
|
|
|
if len(conversationKinds) == 0 {
|
|
|
|
|
conversationKinds = json.RawMessage(`[]`)
|
|
|
|
|
}
|
|
|
|
|
customerColors := json.RawMessage(cache.CustomerColorKinds)
|
|
|
|
|
if len(customerColors) == 0 {
|
|
|
|
|
customerColors = json.RawMessage(`[]`)
|
|
|
|
|
}
|
|
|
|
|
return shangwutongClassificationCacheResponse{
|
|
|
|
|
InboxID: cache.InboxID, ConversationKinds: conversationKinds, CustomerColorKinds: customerColors,
|
|
|
|
|
SyncStatus: cache.SyncStatus, SyncedAt: cache.SyncedAt, LastErrorCode: cache.LastErrorCode, LastErrorMessage: cache.LastErrorMessage,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validClassificationCatalog(request shangwutongClassificationCallbackRequest) bool {
|
|
|
|
|
if len(request.ConversationKinds) > 256 || len(request.CustomerColorKinds) > 256 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
seen := make(map[string]struct{}, len(request.ConversationKinds)+len(request.CustomerColorKinds))
|
|
|
|
|
for _, kind := range request.ConversationKinds {
|
|
|
|
|
if strings.TrimSpace(kind.ID) == "" || strings.TrimSpace(kind.Name) == "" || len(kind.ID) > 128 || len(kind.Name) > 255 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
key := "conversation:" + kind.ID
|
|
|
|
|
if _, ok := seen[key]; ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
seen[key] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
for _, color := range request.CustomerColorKinds {
|
|
|
|
|
if strings.TrimSpace(color.ID) == "" || strings.TrimSpace(color.Name) == "" || len(color.ID) > 128 || len(color.Name) > 255 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
key := "color:" + color.ID
|
|
|
|
|
if _, ok := seen[key]; ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
seen[key] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ShangwutongConnectorHandler) inboxItem(c *gin.Context, inbox *model.Inbox) (shangwutongConnectorInbox, error) {
|
|
|
|
|
var config model.ChannelShangwutongConfig
|
|
|
|
|
if err := h.db.WithContext(c.Request.Context()).Where("inbox_id = ?", inbox.ID).First(&config).Error; err != nil {
|
|
|
|
|