211 lines
6.6 KiB
Go
211 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ContactInboxService implements business logic for ContactInbox operations.
|
|
// Reference: Chatwoot app/models/contact_inbox.rb
|
|
type ContactInboxService struct {
|
|
repo *repository.ContactInboxRepo
|
|
}
|
|
|
|
// NewContactInboxService creates a new ContactInbox service.
|
|
func NewContactInboxService(repo *repository.ContactInboxRepo) *ContactInboxService {
|
|
return &ContactInboxService{repo: repo}
|
|
}
|
|
|
|
// GetByID retrieves a single contact_inbox.
|
|
func (s *ContactInboxService) GetByID(ctx context.Context, id uint) (*model.ContactInbox, error) {
|
|
return s.repo.FindByID(ctx, id)
|
|
}
|
|
|
|
// GetByContactAndInbox retrieves a contact_inbox by contact and inbox.
|
|
func (s *ContactInboxService) GetByContactAndInbox(ctx context.Context, contactID, inboxID uint) (*model.ContactInbox, error) {
|
|
return s.repo.FindByContactAndInbox(ctx, contactID, inboxID)
|
|
}
|
|
|
|
// ListByContact retrieves all contact_inboxes for a contact.
|
|
func (s *ContactInboxService) ListByContact(ctx context.Context, contactID uint) ([]model.ContactInbox, error) {
|
|
return s.repo.FindByContact(ctx, contactID)
|
|
}
|
|
|
|
// ListByInbox retrieves all contact_inboxes for an inbox (paginated).
|
|
func (s *ContactInboxService) ListByInbox(ctx context.Context, inboxID uint, offset, limit int) ([]model.ContactInbox, int64, error) {
|
|
return s.repo.FindByInbox(ctx, inboxID, offset, limit)
|
|
}
|
|
|
|
// GetBySourceID retrieves a contact_inbox by source_id and inbox.
|
|
func (s *ContactInboxService) GetBySourceID(ctx context.Context, inboxID uint, sourceID string) (*model.ContactInbox, error) {
|
|
return s.repo.FindBySourceID(ctx, inboxID, sourceID)
|
|
}
|
|
|
|
// CreateContactInboxRequest is the DTO for creating a contact_inbox.
|
|
type CreateContactInboxRequest struct {
|
|
ContactID uint `json:"contact_id"`
|
|
InboxID uint `json:"inbox_id"`
|
|
SourceID string `json:"source_id"`
|
|
HMACVerified bool `json:"hmac_verified"`
|
|
Contact *model.Contact `json:"-"`
|
|
Inbox *model.Inbox `json:"-"`
|
|
}
|
|
|
|
// Create adds a new contact_inbox join record.
|
|
func (s *ContactInboxService) Create(ctx context.Context, req CreateContactInboxRequest) (*model.ContactInbox, error) {
|
|
if req.ContactID == 0 || req.InboxID == 0 {
|
|
return nil, errors.New("contact_id and inbox_id are required")
|
|
}
|
|
|
|
sourceID := strings.TrimSpace(req.SourceID)
|
|
if sourceID == "" {
|
|
generated, err := generateContactInboxSourceID(req.Contact, req.Inbox)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceID = generated
|
|
}
|
|
if sourceID == "" {
|
|
return nil, errors.New("source_id is required")
|
|
}
|
|
|
|
existing, err := s.repo.FindByContactInboxSource(ctx, req.ContactID, req.InboxID, sourceID)
|
|
if err == nil && existing != nil {
|
|
return existing, nil
|
|
}
|
|
|
|
hmacToken, err := generateToken(24)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pubsubToken, err := generateToken(24)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ci := &model.ContactInbox{
|
|
ContactID: req.ContactID,
|
|
InboxID: req.InboxID,
|
|
SourceID: sourceID,
|
|
HMACToken: hmacToken,
|
|
PubsubToken: pubsubToken,
|
|
HMACVerified: req.HMACVerified,
|
|
}
|
|
if req.Inbox != nil {
|
|
ci.Inbox = *req.Inbox
|
|
}
|
|
if err := s.repo.Create(ctx, ci); err != nil {
|
|
applogger.L().Errorf("ContactInboxService.Create failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return ci, nil
|
|
}
|
|
|
|
func generateContactInboxSourceID(contact *model.Contact, inbox *model.Inbox) (string, error) {
|
|
if contact == nil || inbox == nil {
|
|
return "", errors.New("contact and inbox are required to generate source_id")
|
|
}
|
|
switch normalizedInboxChannelType(inbox.ChannelType) {
|
|
case "api", "web_widget":
|
|
return uuid.NewString(), nil
|
|
case "email":
|
|
if strings.TrimSpace(contact.Email) == "" {
|
|
return "", errors.New("contact email is required")
|
|
}
|
|
return contact.Email, nil
|
|
case "sms":
|
|
return contactPhoneSourceID(contact)
|
|
case "whatsapp":
|
|
phone, err := contactPhoneSourceID(contact)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.ReplaceAll(phone, "+", ""), nil
|
|
case "twilio_sms":
|
|
phone, err := contactPhoneSourceID(contact)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if twilioInboxMedium(inbox) == "whatsapp" {
|
|
return fmt.Sprintf("whatsapp:%s", phone), nil
|
|
}
|
|
return phone, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported operation for this channel: %s", inbox.ChannelType)
|
|
}
|
|
}
|
|
|
|
func contactPhoneSourceID(contact *model.Contact) (string, error) {
|
|
if strings.TrimSpace(contact.PhoneNumber) == "" {
|
|
return "", errors.New("contact phone number is required")
|
|
}
|
|
return contact.PhoneNumber, nil
|
|
}
|
|
|
|
func normalizedInboxChannelType(channelType string) string {
|
|
normalized := strings.TrimSpace(channelType)
|
|
normalized = strings.TrimPrefix(normalized, "Channel::")
|
|
normalized = strings.ToLower(normalized)
|
|
normalized = strings.ReplaceAll(normalized, "_", "")
|
|
switch normalized {
|
|
case "webwidget":
|
|
return "web_widget"
|
|
case "twiliosms":
|
|
return "twilio_sms"
|
|
case "api", "email", "sms", "whatsapp":
|
|
return normalized
|
|
default:
|
|
return strings.ToLower(channelType)
|
|
}
|
|
}
|
|
|
|
func twilioInboxMedium(inbox *model.Inbox) string {
|
|
if inbox == nil || strings.TrimSpace(inbox.ChannelConfig) == "" {
|
|
return "sms"
|
|
}
|
|
var config map[string]any
|
|
if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil {
|
|
return "sms"
|
|
}
|
|
if medium, ok := config["medium"].(string); ok && strings.TrimSpace(medium) != "" {
|
|
return strings.ToLower(strings.TrimSpace(medium))
|
|
}
|
|
return "sms"
|
|
}
|
|
|
|
// Delete removes a contact_inbox by ID.
|
|
func (s *ContactInboxService) Delete(ctx context.Context, id uint) error {
|
|
return s.repo.Delete(ctx, id)
|
|
}
|
|
|
|
// DeleteByContactAndInbox removes the join record.
|
|
func (s *ContactInboxService) DeleteByContactAndInbox(ctx context.Context, contactID, inboxID uint) error {
|
|
return s.repo.DeleteByContactAndInbox(ctx, contactID, inboxID)
|
|
}
|
|
|
|
// FilterContactInboxes retrieves contact_inboxes by account with optional filters.
|
|
// GET /api/v1/accounts/:id/contact_inboxes/filter
|
|
// Reference: Chatwoot contact_inboxes_controller#filter
|
|
func (s *ContactInboxService) FilterContactInboxes(ctx context.Context, accountID uint, inboxID, contactID *uint, sourceID string, offset, limit int) ([]model.ContactInbox, int64, error) {
|
|
return s.repo.Filter(ctx, accountID, inboxID, contactID, sourceID, offset, limit)
|
|
}
|
|
|
|
// generateToken creates a random hex token of the specified byte length.
|
|
func generateToken(byteLen int) (string, error) {
|
|
b := make([]byte, byteLen)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|