chore: import clean Agent Desk snapshot (3/7)
This commit is contained in:
426
internal/services/channel_message_outbox_service.go
Normal file
426
internal/services/channel_message_outbox_service.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
var ChannelMessageOutboxService = newChannelMessageOutboxService()
|
||||
|
||||
func newChannelMessageOutboxService() *channelMessageOutboxService {
|
||||
return &channelMessageOutboxService{}
|
||||
}
|
||||
|
||||
type channelMessageOutboxService struct {
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Get(id int64) *models.ChannelMessageOutbox {
|
||||
return repositories.ChannelMessageOutboxRepository.Get(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Take(where ...interface{}) *models.ChannelMessageOutbox {
|
||||
return repositories.ChannelMessageOutboxRepository.Take(sqls.DB(), where...)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Find(cnd *sqls.Cnd) []models.ChannelMessageOutbox {
|
||||
return repositories.ChannelMessageOutboxRepository.Find(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) FindOne(cnd *sqls.Cnd) *models.ChannelMessageOutbox {
|
||||
return repositories.ChannelMessageOutboxRepository.FindOne(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) FindPageByParams(params *params.QueryParams) (list []models.ChannelMessageOutbox, paging *sqls.Paging) {
|
||||
return repositories.ChannelMessageOutboxRepository.FindPageByParams(sqls.DB(), params)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) FindPageByCnd(cnd *sqls.Cnd) (list []models.ChannelMessageOutbox, paging *sqls.Paging) {
|
||||
return repositories.ChannelMessageOutboxRepository.FindPageByCnd(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Count(cnd *sqls.Cnd) int64 {
|
||||
return repositories.ChannelMessageOutboxRepository.Count(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Create(t *models.ChannelMessageOutbox) error {
|
||||
return repositories.ChannelMessageOutboxRepository.Create(sqls.DB(), t)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Update(t *models.ChannelMessageOutbox) error {
|
||||
return repositories.ChannelMessageOutboxRepository.Update(sqls.DB(), t)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Updates(id int64, columns map[string]interface{}) error {
|
||||
return repositories.ChannelMessageOutboxRepository.Updates(sqls.DB(), id, columns)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) UpdateColumn(id int64, name string, value interface{}) error {
|
||||
return repositories.ChannelMessageOutboxRepository.UpdateColumn(sqls.DB(), id, name, value)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) Delete(id int64) {
|
||||
repositories.ChannelMessageOutboxRepository.Delete(sqls.DB(), id)
|
||||
}
|
||||
|
||||
// GetByMessageID retrieves the outbox entry by message ID and channel type.
|
||||
func (s *channelMessageOutboxService) GetByMessageID(channelType string, messageID int64) *models.ChannelMessageOutbox {
|
||||
return repositories.ChannelMessageOutboxRepository.Take(sqls.DB(), "channel_type = ? AND message_id = ?", channelType, messageID)
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) EnqueueTelegramMessage(conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil || message == nil {
|
||||
return nil
|
||||
}
|
||||
channel := ChannelService.Get(conversation.ChannelID)
|
||||
if channel == nil || channel.ChannelType != enums.ChannelTypeTelegram {
|
||||
return nil
|
||||
}
|
||||
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
|
||||
return nil
|
||||
}
|
||||
if message.MessageType != enums.IMMessageTypeText && message.MessageType != enums.IMMessageTypeHTML {
|
||||
return nil
|
||||
}
|
||||
if existing := s.GetByMessageID(enums.ChannelTypeTelegram, message.ID); existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"conversationId": conversation.ID,
|
||||
"messageId": message.ID,
|
||||
"messageType": message.MessageType,
|
||||
"content": strings.TrimSpace(message.Content),
|
||||
"payload": strings.TrimSpace(message.Payload),
|
||||
"senderId": message.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.Create(&models.ChannelMessageOutbox{
|
||||
ChannelType: enums.ChannelTypeTelegram,
|
||||
ConversationID: conversation.ID,
|
||||
MessageID: message.ID,
|
||||
Payload: string(payload),
|
||||
SendStatus: string(enums.ChannelMessageOutboxStatusPending),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: message.UpdateUserID,
|
||||
CreateUserName: message.UpdateUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: message.UpdateUserID,
|
||||
UpdateUserName: message.UpdateUserName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("recovered from panic in telegram outbound dispatch", "error", r)
|
||||
}
|
||||
}()
|
||||
TelegramOutboundService.DispatchPendingOutbox()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) EnqueueZaloOAMessage(conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil || message == nil {
|
||||
return nil
|
||||
}
|
||||
channel := ChannelService.Get(conversation.ChannelID)
|
||||
if channel == nil || channel.ChannelType != enums.ChannelTypeZaloOA {
|
||||
return nil
|
||||
}
|
||||
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
|
||||
return nil
|
||||
}
|
||||
if message.MessageType != enums.IMMessageTypeText && message.MessageType != enums.IMMessageTypeHTML {
|
||||
return nil
|
||||
}
|
||||
if existing := s.GetByMessageID(enums.ChannelTypeZaloOA, message.ID); existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"conversationId": conversation.ID,
|
||||
"messageId": message.ID,
|
||||
"messageType": message.MessageType,
|
||||
"content": strings.TrimSpace(message.Content),
|
||||
"payload": strings.TrimSpace(message.Payload),
|
||||
"senderId": message.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.Create(&models.ChannelMessageOutbox{
|
||||
ChannelType: enums.ChannelTypeZaloOA,
|
||||
ConversationID: conversation.ID,
|
||||
MessageID: message.ID,
|
||||
Payload: string(payload),
|
||||
SendStatus: string(enums.ChannelMessageOutboxStatusPending),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: message.UpdateUserID,
|
||||
CreateUserName: message.UpdateUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: message.UpdateUserID,
|
||||
UpdateUserName: message.UpdateUserName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("recovered from panic in zalo oa outbound dispatch", "error", r)
|
||||
}
|
||||
}()
|
||||
ZaloOAOutboundService.DispatchPendingOutbox()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) EnqueueSlackMessage(conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil || message == nil {
|
||||
return nil
|
||||
}
|
||||
channel := ChannelService.Get(conversation.ChannelID)
|
||||
if channel == nil || channel.ChannelType != enums.ChannelTypeSlack {
|
||||
return nil
|
||||
}
|
||||
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
|
||||
return nil
|
||||
}
|
||||
if message.MessageType != enums.IMMessageTypeText && message.MessageType != enums.IMMessageTypeHTML && message.MessageType != enums.IMMessageTypeImage && message.MessageType != enums.IMMessageTypeAttachment {
|
||||
return nil
|
||||
}
|
||||
if existing := s.GetByMessageID(enums.ChannelTypeSlack, message.ID); existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"conversationId": conversation.ID,
|
||||
"messageId": message.ID,
|
||||
"messageType": message.MessageType,
|
||||
"content": strings.TrimSpace(message.Content),
|
||||
"payload": strings.TrimSpace(message.Payload),
|
||||
"senderId": message.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.Create(&models.ChannelMessageOutbox{
|
||||
ChannelType: enums.ChannelTypeSlack,
|
||||
ConversationID: conversation.ID,
|
||||
MessageID: message.ID,
|
||||
Payload: string(payload),
|
||||
SendStatus: string(enums.ChannelMessageOutboxStatusPending),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: message.UpdateUserID,
|
||||
CreateUserName: message.UpdateUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: message.UpdateUserID,
|
||||
UpdateUserName: message.UpdateUserName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("recovered from panic in slack outbound dispatch", "error", r)
|
||||
}
|
||||
}()
|
||||
SlackOutboundService.DispatchPendingOutbox()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) EnqueueEmailMessage(conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.EnqueueEmailMessageVia(ChannelService.Get(conversation.ChannelID), conversation, message)
|
||||
}
|
||||
|
||||
// EnqueueEmailMessageVia queues a message for email delivery through an explicit
|
||||
// channel.
|
||||
//
|
||||
// The channel is a parameter because the delivery medium and the conversation's
|
||||
// channel are not always the same thing. Telling a visitor that a request they
|
||||
// sent by email landed in a chat they left has to go out over email even though
|
||||
// that conversation belongs to the web channel; deriving the channel from the
|
||||
// conversation, as the original code did, silently dropped exactly those notices.
|
||||
func (s *channelMessageOutboxService) EnqueueEmailMessageVia(channel *models.Channel, conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil || message == nil {
|
||||
return nil
|
||||
}
|
||||
if channel == nil || channel.ChannelType != enums.ChannelTypeEmail {
|
||||
return nil
|
||||
}
|
||||
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
|
||||
return nil
|
||||
}
|
||||
if message.MessageType != enums.IMMessageTypeText && message.MessageType != enums.IMMessageTypeHTML {
|
||||
return nil
|
||||
}
|
||||
if existing := s.GetByMessageID(enums.ChannelTypeEmail, message.ID); existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"conversationId": conversation.ID,
|
||||
"messageId": message.ID,
|
||||
"messageType": message.MessageType,
|
||||
"content": strings.TrimSpace(message.Content),
|
||||
"payload": strings.TrimSpace(message.Payload),
|
||||
"senderId": message.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.Create(&models.ChannelMessageOutbox{
|
||||
ChannelType: enums.ChannelTypeEmail,
|
||||
ConversationID: conversation.ID,
|
||||
MessageID: message.ID,
|
||||
Payload: string(payload),
|
||||
SendStatus: string(enums.ChannelMessageOutboxStatusPending),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: message.UpdateUserID,
|
||||
CreateUserName: message.UpdateUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: message.UpdateUserID,
|
||||
UpdateUserName: message.UpdateUserName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Trigger async dispatch immediately
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("recovered from panic in email outbound dispatch", "error", r)
|
||||
}
|
||||
}()
|
||||
EmailOutboundService.DispatchPendingOutbox()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) EnqueueDiscordMessage(conversation *models.Conversation, message *models.Message) error {
|
||||
if conversation == nil || message == nil {
|
||||
return nil
|
||||
}
|
||||
channel := ChannelService.Get(conversation.ChannelID)
|
||||
if channel == nil || channel.ChannelType != enums.ChannelTypeDiscord {
|
||||
return nil
|
||||
}
|
||||
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
|
||||
return nil
|
||||
}
|
||||
if message.MessageType != enums.IMMessageTypeText && message.MessageType != enums.IMMessageTypeHTML && message.MessageType != enums.IMMessageTypeImage && message.MessageType != enums.IMMessageTypeAttachment {
|
||||
return nil
|
||||
}
|
||||
if existing := s.GetByMessageID(enums.ChannelTypeDiscord, message.ID); existing != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"conversationId": conversation.ID,
|
||||
"messageId": message.ID,
|
||||
"messageType": message.MessageType,
|
||||
"content": strings.TrimSpace(message.Content),
|
||||
"payload": strings.TrimSpace(message.Payload),
|
||||
"senderId": message.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.Create(&models.ChannelMessageOutbox{
|
||||
ChannelType: enums.ChannelTypeDiscord,
|
||||
ConversationID: conversation.ID,
|
||||
MessageID: message.ID,
|
||||
Payload: string(payload),
|
||||
SendStatus: string(enums.ChannelMessageOutboxStatusPending),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: message.UpdateUserID,
|
||||
CreateUserName: message.UpdateUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: message.UpdateUserID,
|
||||
UpdateUserName: message.UpdateUserName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("recovered from panic in discord outbound dispatch", "error", r)
|
||||
}
|
||||
}()
|
||||
DiscordOutboundService.DispatchPendingOutbox()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *channelMessageOutboxService) ListPending(channelType string, limit int) []models.ChannelMessageOutbox {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
now := time.Now()
|
||||
cnd := sqls.NewCnd().
|
||||
Eq("channel_type", strings.TrimSpace(channelType)).
|
||||
In("send_status", []string{
|
||||
string(enums.ChannelMessageOutboxStatusPending),
|
||||
string(enums.ChannelMessageOutboxStatusFailed),
|
||||
}).
|
||||
Asc("next_retry_at").
|
||||
Asc("id").
|
||||
Limit(limit)
|
||||
// A row with no next_retry_at has never been scheduled, so it is due
|
||||
// immediately. Comparing it directly would not work: NULL <= ? is unknown in
|
||||
// SQL, never true, so a plain Lte silently drops every freshly queued message
|
||||
// for every channel. Only rows whose backoff has elapsed are held back, and
|
||||
// ordering by next_retry_at keeps a backlog of not-yet-due retries from
|
||||
// starving newer pending sends.
|
||||
cnd.Where("next_retry_at IS NULL OR next_retry_at <= ?", now)
|
||||
return s.Find(cnd)
|
||||
}
|
||||
Reference in New Issue
Block a user