Files
gochat/internal/handler/webhook/incoming_persister_jobs.go
T

247 lines
8.2 KiB
Go

package webhook
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/worker"
)
const (
TaskTypeProviderIncomingMessagePersist = "webhook:incoming_message_persist"
TaskTypeProviderMessageStatusUpdate = "webhook:message_status_update"
TaskTypeProviderContactMessagesStatusUpdate = "webhook:contact_messages_status_update"
)
type providerIncomingMessagePersistJob struct {
InboxID uint `json:"inbox_id"`
Message channel.IncomingMessage `json:"message"`
}
type providerMessageStatusUpdateJob struct {
InboxID uint `json:"inbox_id"`
SourceID string `json:"source_id"`
Status model.MessageStatus `json:"status"`
OccurredAtUnixNano int64 `json:"occurred_at_unix_nano,omitempty"`
ExternalError string `json:"external_error,omitempty"`
}
type providerContactMessagesStatusUpdateJob struct {
InboxID uint `json:"inbox_id"`
ContactSourceID string `json:"contact_source_id"`
Status model.MessageStatus `json:"status"`
OccurredAtUnixNano int64 `json:"occurred_at_unix_nano,omitempty"`
}
func (p *IncomingPersister) registerJobs(wp *worker.WorkerPool) {
if p == nil || wp == nil {
return
}
wp.Register(TaskTypeProviderIncomingMessagePersist, p.performIncomingMessagePersistJob)
wp.Register(TaskTypeProviderMessageStatusUpdate, p.performMessageStatusUpdateJob)
wp.Register(TaskTypeProviderContactMessagesStatusUpdate, p.performContactMessagesStatusUpdateJob)
}
func (p *IncomingPersister) enqueueIncomingMessagePersist(ctx context.Context, inboxID uint, msg *channel.IncomingMessage) error {
if p == nil || p.worker == nil || inboxID == 0 || msg == nil {
return nil
}
queue := incomingMessageQueue(msg.ChannelType)
_, err := p.worker.Enqueue(ctx, TaskTypeProviderIncomingMessagePersist, providerIncomingMessagePersistJob{InboxID: inboxID, Message: *msg},
worker.WithQueue(queue),
worker.WithMaxAttempts(3),
worker.WithIdempotencyKey(fmt.Sprintf("webhook:incoming_message:%d:%s", inboxID, msg.SourceID)),
)
return err
}
func (p *IncomingPersister) enqueueMessageStatusUpdate(ctx context.Context, inboxID uint, sourceID string, status model.MessageStatus, occurredAt *time.Time, externalError string) error {
if p == nil || p.worker == nil || inboxID == 0 || sourceID == "" {
return nil
}
if !validProviderMessageStatus(status) {
return nil
}
payload := providerMessageStatusUpdateJob{
InboxID: inboxID,
SourceID: sourceID,
Status: status,
OccurredAtUnixNano: timeToUnixNano(occurredAt),
ExternalError: strings.TrimSpace(externalError),
}
_, err := p.worker.Enqueue(ctx, TaskTypeProviderMessageStatusUpdate, payload,
worker.WithQueue("low"),
worker.WithMaxAttempts(3),
worker.WithIdempotencyKey(fmt.Sprintf("webhook:message_status:%d:%s:%s:%d", inboxID, sourceID, status, payload.OccurredAtUnixNano)),
)
return err
}
func (p *IncomingPersister) enqueueContactMessagesStatusUpdate(ctx context.Context, inboxID uint, contactSourceID string, status model.MessageStatus, occurredAt *time.Time) error {
if p == nil || p.worker == nil || inboxID == 0 || contactSourceID == "" {
return nil
}
if !validProviderMessageStatus(status) {
return nil
}
payload := providerContactMessagesStatusUpdateJob{
InboxID: inboxID,
ContactSourceID: contactSourceID,
Status: status,
OccurredAtUnixNano: timeToUnixNano(occurredAt),
}
_, err := p.worker.Enqueue(ctx, TaskTypeProviderContactMessagesStatusUpdate, payload,
worker.WithQueue("low"),
worker.WithMaxAttempts(3),
worker.WithIdempotencyKey(fmt.Sprintf("webhook:contact_messages_status:%d:%s:%s:%d", inboxID, contactSourceID, status, payload.OccurredAtUnixNano)),
)
return err
}
func (p *IncomingPersister) performMessageStatusUpdateJob(ctx context.Context, job *model.BackgroundJob) error {
var payload providerMessageStatusUpdateJob
if err := json.Unmarshal(job.Payload, &payload); err != nil {
return fmt.Errorf("unmarshal provider message status job: %w", err)
}
if payload.InboxID == 0 || payload.SourceID == "" || !validProviderMessageStatus(payload.Status) {
return fmt.Errorf("invalid provider message status job payload: %#v", payload)
}
inbox, err := p.loadInboxForStatusJob(ctx, payload.InboxID)
if err != nil {
return err
}
if inbox == nil {
return nil
}
return p.performMessageStatusUpdate(ctx, inbox, payload.SourceID, payload.Status, unixNanoToTime(payload.OccurredAtUnixNano), payload.ExternalError)
}
func (p *IncomingPersister) performIncomingMessagePersistJob(ctx context.Context, job *model.BackgroundJob) error {
var payload providerIncomingMessagePersistJob
if err := json.Unmarshal(job.Payload, &payload); err != nil {
return fmt.Errorf("unmarshal provider incoming message job: %w", err)
}
if payload.InboxID == 0 {
return fmt.Errorf("invalid provider incoming message job payload: %#v", payload)
}
inbox, err := p.loadInboxForStatusJob(ctx, payload.InboxID)
if err != nil {
return err
}
if inbox == nil {
return nil
}
if err := validateIncomingMessage(p, inbox, &payload.Message); err != nil {
return err
}
_, err = p.performPersistIncoming(ctx, inbox, &payload.Message)
return err
}
func (p *IncomingPersister) performContactMessagesStatusUpdateJob(ctx context.Context, job *model.BackgroundJob) error {
var payload providerContactMessagesStatusUpdateJob
if err := json.Unmarshal(job.Payload, &payload); err != nil {
return fmt.Errorf("unmarshal provider contact messages status job: %w", err)
}
if payload.InboxID == 0 || payload.ContactSourceID == "" || !validProviderMessageStatus(payload.Status) {
return fmt.Errorf("invalid provider contact messages status job payload: %#v", payload)
}
inbox, err := p.loadInboxForStatusJob(ctx, payload.InboxID)
if err != nil {
return err
}
if inbox == nil {
return nil
}
return p.performContactMessagesStatusUpdate(ctx, inbox, payload.ContactSourceID, payload.Status, unixNanoToTime(payload.OccurredAtUnixNano))
}
func (p *IncomingPersister) loadInboxForStatusJob(ctx context.Context, inboxID uint) (*model.Inbox, error) {
var inbox model.Inbox
if err := p.db.WithContext(ctx).First(&inbox, inboxID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, fmt.Errorf("load status inbox %d: %w", inboxID, err)
}
return &inbox, nil
}
func validProviderMessageStatus(status model.MessageStatus) bool {
switch status {
case model.MessageStatusSent, model.MessageStatusDelivered, model.MessageStatusRead, model.MessageStatusFailed:
return true
default:
return false
}
}
func incomingMessageQueue(channelType channel.ChannelType) string {
switch channelType {
case channel.ChannelWhatsApp, channel.ChannelTwilioSMS, channel.ChannelTwilioWA:
return "low"
default:
return model.DefaultBackgroundJobQueue
}
}
func validProviderMessageStatusTransition(current, next model.MessageStatus) bool {
if !validProviderMessageStatus(next) || current == next {
return validProviderMessageStatus(next)
}
if next == model.MessageStatusFailed || current == model.MessageStatusFailed {
return true
}
return providerMessageStatusRank(next) >= providerMessageStatusRank(current)
}
func providerMessageStatusRank(status model.MessageStatus) int {
switch status {
case model.MessageStatusSent:
return 1
case model.MessageStatusDelivered:
return 2
case model.MessageStatusRead:
return 3
default:
return 0
}
}
func setProviderMessageExternalError(attrs datatypes.JSON, status model.MessageStatus, externalError string) datatypes.JSON {
obj := map[string]any{}
if len(attrs) > 0 {
_ = json.Unmarshal(attrs, &obj)
}
if status == model.MessageStatusFailed && strings.TrimSpace(externalError) != "" {
obj["external_error"] = strings.TrimSpace(externalError)
} else {
delete(obj, "external_error")
}
bytes, _ := json.Marshal(obj)
return datatypes.JSON(bytes)
}
func timeToUnixNano(t *time.Time) int64 {
if t == nil || t.IsZero() {
return 0
}
return t.UTC().UnixNano()
}
func unixNanoToTime(value int64) *time.Time {
if value <= 0 {
return nil
}
t := time.Unix(0, value).UTC()
return &t
}