Files
gochat/internal/service/conversation_participant_service.go
T
2026-06-04 15:44:48 +08:00

149 lines
5.9 KiB
Go

package service
import (
"context"
"fmt"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
)
// ConversationParticipantService implements business logic for ConversationParticipant operations.
// Reference: Chatwoot app/controllers/api/v1/accounts/conversations/participants_controller.rb
// Chatwoot validates inbox access via assignable_agents before allowing participants.
type ConversationParticipantService struct {
repo *repository.ConversationParticipantRepo
conversationRepo *repository.ConversationRepo
assignableAgentSvc *AssignableAgentService
}
// NewConversationParticipantService creates a new ConversationParticipant service.
func NewConversationParticipantService(repo *repository.ConversationParticipantRepo, conversationRepo *repository.ConversationRepo) *ConversationParticipantService {
return &ConversationParticipantService{repo: repo, conversationRepo: conversationRepo}
}
// SetAssignableAgentService injects the assignable agent service for inbox access validation.
func (s *ConversationParticipantService) SetAssignableAgentService(svc *AssignableAgentService) {
s.assignableAgentSvc = svc
}
// List retrieves all participants for a conversation.
// Chatwoot: @participants = @conversation.conversation_participants
func (s *ConversationParticipantService) List(ctx context.Context, accountID, conversationID uint) ([]model.ConversationParticipant, error) {
if _, err := s.conversationRepo.FindByAccountAndID(ctx, accountID, conversationID); err != nil {
return nil, err
}
return s.repo.FindByConversationID(ctx, conversationID)
}
// Add adds a participant to a conversation.
// Chatwoot: find_or_create_by(user_id), validates inbox access via assignable_agents
func (s *ConversationParticipantService) Add(ctx context.Context, accountID, conversationID, userID uint, role string) (*model.ConversationParticipant, error) {
conversation, err := s.conversationRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
// Chatwoot: validates :ensure_inbox_access — user must be in assignable_agents
if s.assignableAgentSvc != nil {
if err := s.validateInboxAccess(ctx, accountID, conversation.InboxID, userID); err != nil {
return nil, err
}
}
// Chatwoot: before_validation :ensure_account_id — auto-sets account_id from conversation
// find_or_create_by pattern
existing, err := s.repo.FindByConversationAndUserID(ctx, conversationID, userID)
if err == nil && existing != nil {
return existing, nil
}
participant := &model.ConversationParticipant{
AccountID: accountID,
ConversationID: conversationID,
UserID: userID,
Role: role,
}
if err := s.repo.Create(ctx, participant); err != nil {
applogger.L().Errorf("ConversationParticipantService.Add: %v", err)
return nil, err
}
return participant, nil
}
// Update updates a participant's role in a conversation.
func (s *ConversationParticipantService) Update(ctx context.Context, accountID, conversationID, userID uint, role string) (*model.ConversationParticipant, error) {
if _, err := s.conversationRepo.FindByAccountAndID(ctx, accountID, conversationID); err != nil {
return nil, err
}
if err := s.repo.UpdateRole(ctx, conversationID, userID, role); err != nil {
applogger.L().Errorf("ConversationParticipantService.Update: %v", err)
return nil, err
}
return s.repo.FindByConversationAndUserID(ctx, conversationID, userID)
}
// Remove removes a participant from a conversation.
// Chatwoot: conversation_participants.find_by(user_id: user_id)&.destroy
func (s *ConversationParticipantService) Remove(ctx context.Context, accountID, conversationID, userID uint) error {
if _, err := s.conversationRepo.FindByAccountAndID(ctx, accountID, conversationID); err != nil {
return err
}
return s.repo.DeleteByConversationAndUserID(ctx, conversationID, userID)
}
// BatchUpdate adds and removes participants in bulk.
// Chatwoot: update action — adds via find_or_create_by, removes via destroy
func (s *ConversationParticipantService) BatchUpdate(ctx context.Context, accountID, conversationID uint, addUserIDs []uint, removeUserIDs []uint, role string) ([]model.ConversationParticipant, error) {
conversation, err := s.conversationRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
// Add new participants (find_or_create_by pattern)
for _, userID := range addUserIDs {
if s.assignableAgentSvc != nil {
if err := s.validateInboxAccess(ctx, accountID, conversation.InboxID, userID); err != nil {
return nil, err
}
}
existing, _ := s.repo.FindByConversationAndUserID(ctx, conversationID, userID)
if existing != nil {
continue
}
participant := &model.ConversationParticipant{
AccountID: accountID,
ConversationID: conversationID,
UserID: userID,
Role: role,
}
if err := s.repo.Create(ctx, participant); err != nil {
applogger.L().Errorf("BatchUpdate add: %v", err)
}
}
// Remove old participants
for _, userID := range removeUserIDs {
if err := s.repo.DeleteByConversationAndUserID(ctx, conversationID, userID); err != nil {
applogger.L().Errorf("BatchUpdate remove user %d: %v", userID, err)
}
}
return s.repo.FindByConversationID(ctx, conversationID)
}
// validateInboxAccess checks that a user has access to the conversation's inbox.
// Chatwoot: errors.add(:user, 'must have inbox access') if conversation.inbox.assignable_agents.exclude?(user)
func (s *ConversationParticipantService) validateInboxAccess(ctx context.Context, accountID, inboxID, userID uint) error {
agents, err := s.assignableAgentSvc.FindAssignableAgents(ctx, accountID, []uint{inboxID})
if err != nil {
return fmt.Errorf("failed to verify inbox access: %w", err)
}
for _, agent := range agents {
if agent.ID == userID {
return nil
}
}
return fmt.Errorf("user %d must have inbox access", userID)
}