424 lines
14 KiB
Go
424 lines
14 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time" // Add time import
|
|
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/repositories"
|
|
|
|
"github.com/google/uuid" // Add uuid import
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RoomService gère la logique métier pour les rooms
|
|
type RoomService struct {
|
|
roomRepo *repositories.RoomRepository
|
|
messageRepo *repositories.ChatMessageRepository
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewRoomService crée une nouvelle instance de RoomService
|
|
func NewRoomService(roomRepo *repositories.RoomRepository, messageRepo *repositories.ChatMessageRepository, logger *zap.Logger) *RoomService {
|
|
return &RoomService{
|
|
roomRepo: roomRepo,
|
|
messageRepo: messageRepo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// CreateRoomRequest représente une requête de création de room
|
|
type CreateRoomRequest struct {
|
|
Name string `json:"name" binding:"required,min=1,max=255"`
|
|
Description *string `json:"description,omitempty"`
|
|
Type string `json:"type" binding:"required,oneof=public private direct"`
|
|
IsPrivate bool `json:"is_private"`
|
|
}
|
|
|
|
// RoomResponse représente une réponse de room pour l'API
|
|
// MIGRATION UUID: CreatedBy et Participants migrés vers UUID
|
|
type RoomResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Type string `json:"type"`
|
|
IsPrivate bool `json:"is_private"`
|
|
CreatedBy *uuid.UUID `json:"created_by"`
|
|
Participants []uuid.UUID `json:"participants"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// CreateRoom crée une nouvelle room
|
|
func (s *RoomService) CreateRoom(ctx context.Context, userID uuid.UUID, req CreateRoomRequest) (*RoomResponse, error) {
|
|
if req.Name == "" {
|
|
return nil, errors.New("room name is required")
|
|
}
|
|
|
|
// Créer la room
|
|
room := &models.Room{
|
|
Name: req.Name,
|
|
Description: "",
|
|
Type: req.Type,
|
|
IsPrivate: req.IsPrivate,
|
|
CreatedBy: userID, // Corrected: userID is uuid.UUID, models.Room.CreatedBy is uuid.UUID
|
|
}
|
|
|
|
if req.Description != nil {
|
|
room.Description = *req.Description
|
|
}
|
|
|
|
if err := s.roomRepo.Create(ctx, room); err != nil {
|
|
s.logger.Error("failed to create room",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("room_name", req.Name))
|
|
return nil, fmt.Errorf("failed to create room: %w", err)
|
|
}
|
|
|
|
// Ajouter le créateur comme membre admin
|
|
member := &models.RoomMember{
|
|
RoomID: room.ID, // use uuid
|
|
UserID: userID,
|
|
Role: "admin",
|
|
}
|
|
|
|
if err := s.roomRepo.AddMember(ctx, member); err != nil {
|
|
s.logger.Error("failed to add creator as room member",
|
|
zap.Error(err),
|
|
zap.String("room_id", room.ID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
// Ne pas retourner d'erreur, la room est créée
|
|
}
|
|
|
|
s.logger.Info("room created successfully",
|
|
zap.String("room_id", room.ID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("room_name", room.Name))
|
|
|
|
return &RoomResponse{
|
|
ID: room.ID,
|
|
Name: room.Name,
|
|
Description: room.Description,
|
|
Type: room.Type,
|
|
IsPrivate: room.IsPrivate,
|
|
CreatedBy: &room.CreatedBy, // Corrected: & to get pointer to uuid.UUID
|
|
Participants: []uuid.UUID{userID},
|
|
CreatedAt: room.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: room.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}, nil
|
|
}
|
|
|
|
// GetUserRooms récupère toutes les rooms d'un utilisateur
|
|
func (s *RoomService) GetUserRooms(ctx context.Context, userID uuid.UUID) ([]*RoomResponse, error) {
|
|
rooms, err := s.roomRepo.GetByUserID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get user rooms",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()))
|
|
return nil, fmt.Errorf("failed to get user rooms: %w", err)
|
|
}
|
|
|
|
responses := make([]*RoomResponse, 0, len(rooms))
|
|
for _, room := range rooms {
|
|
// Récupérer les membres pour avoir la liste des participants
|
|
members, err := s.roomRepo.GetMembersByRoomID(ctx, room.ID)
|
|
if err != nil {
|
|
s.logger.Warn("failed to get room members",
|
|
zap.Error(err),
|
|
zap.String("room_id", room.ID.String()))
|
|
members = []*models.RoomMember{}
|
|
}
|
|
|
|
participants := make([]uuid.UUID, 0, len(members))
|
|
for _, member := range members {
|
|
participants = append(participants, member.UserID)
|
|
}
|
|
|
|
responses = append(responses, &RoomResponse{
|
|
ID: room.ID,
|
|
Name: room.Name,
|
|
Description: room.Description,
|
|
Type: room.Type,
|
|
IsPrivate: room.IsPrivate,
|
|
CreatedBy: &room.CreatedBy, // Corrected: & to get pointer to uuid.UUID
|
|
Participants: participants,
|
|
CreatedAt: room.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: room.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
// GetRoom récupère une room par son ID
|
|
func (s *RoomService) GetRoom(ctx context.Context, roomID uuid.UUID) (*RoomResponse, error) {
|
|
room, err := s.roomRepo.GetByID(ctx, roomID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrRoomNotFound
|
|
}
|
|
s.logger.Error("failed to get room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
return nil, fmt.Errorf("failed to get room: %w", err)
|
|
}
|
|
|
|
// Récupérer les membres
|
|
members, err := s.roomRepo.GetMembersByRoomID(ctx, roomID)
|
|
if err != nil {
|
|
s.logger.Warn("failed to get room members",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
members = []*models.RoomMember{}
|
|
}
|
|
|
|
participants := make([]uuid.UUID, 0, len(members))
|
|
for _, member := range members {
|
|
participants = append(participants, member.UserID)
|
|
}
|
|
|
|
return &RoomResponse{
|
|
ID: room.ID,
|
|
Name: room.Name,
|
|
Description: room.Description,
|
|
Type: room.Type,
|
|
IsPrivate: room.IsPrivate,
|
|
CreatedBy: &room.CreatedBy, // Corrected: & to get pointer to uuid.UUID
|
|
Participants: participants,
|
|
CreatedAt: room.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: room.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}, nil
|
|
}
|
|
|
|
// AddMember ajoute un membre à une room
|
|
func (s *RoomService) AddMember(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error {
|
|
member := &models.RoomMember{
|
|
RoomID: roomID,
|
|
UserID: userID,
|
|
Role: "member",
|
|
}
|
|
|
|
if err := s.roomRepo.AddMember(ctx, member); err != nil {
|
|
s.logger.Error("failed to add member to room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
return fmt.Errorf("failed to add member: %w", err)
|
|
}
|
|
|
|
s.logger.Info("member added to room",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveMember retire un membre d'une room
|
|
// BE-API-011: Implement conversation participants endpoints
|
|
func (s *RoomService) RemoveMember(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error {
|
|
// Vérifier que la room existe
|
|
_, err := s.roomRepo.GetByID(ctx, roomID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrRoomNotFound
|
|
}
|
|
return fmt.Errorf("failed to get room: %w", err)
|
|
}
|
|
|
|
// Retirer le membre
|
|
if err := s.roomRepo.RemoveMember(ctx, roomID, userID); err != nil {
|
|
s.logger.Error("failed to remove member from room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
return fmt.Errorf("failed to remove member: %w", err)
|
|
}
|
|
|
|
s.logger.Info("member removed from room",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateRoomRequest représente une requête de mise à jour de room
|
|
// BE-API-012: Implement conversation update endpoint
|
|
type UpdateRoomRequest struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
}
|
|
|
|
// UpdateRoom met à jour une room (nom et/ou description)
|
|
// BE-API-012: Implement conversation update endpoint
|
|
// Seul le créateur de la room ou un admin peut mettre à jour la room
|
|
func (s *RoomService) UpdateRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req UpdateRoomRequest) (*RoomResponse, error) {
|
|
// Vérifier que la room existe
|
|
room, err := s.roomRepo.GetByID(ctx, roomID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrRoomNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get room: %w", err)
|
|
}
|
|
|
|
// Vérifier que l'utilisateur est le créateur de la room
|
|
if room.CreatedBy != userID {
|
|
return nil, fmt.Errorf("forbidden: only room creator can update the room")
|
|
}
|
|
|
|
// Mettre à jour les champs fournis
|
|
if req.Name != nil {
|
|
room.Name = *req.Name
|
|
}
|
|
if req.Description != nil {
|
|
room.Description = *req.Description
|
|
}
|
|
|
|
// Sauvegarder les modifications
|
|
if err := s.roomRepo.Update(ctx, room); err != nil {
|
|
s.logger.Error("failed to update room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
return nil, fmt.Errorf("failed to update room: %w", err)
|
|
}
|
|
|
|
// Récupérer les membres pour la réponse
|
|
members, err := s.roomRepo.GetMembersByRoomID(ctx, roomID)
|
|
if err != nil {
|
|
s.logger.Warn("failed to get room members",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
members = []*models.RoomMember{}
|
|
}
|
|
|
|
participants := make([]uuid.UUID, 0, len(members))
|
|
for _, member := range members {
|
|
participants = append(participants, member.UserID)
|
|
}
|
|
|
|
s.logger.Info("room updated successfully",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
return &RoomResponse{
|
|
ID: room.ID,
|
|
Name: room.Name,
|
|
Description: room.Description,
|
|
Type: room.Type,
|
|
IsPrivate: room.IsPrivate,
|
|
CreatedBy: &room.CreatedBy,
|
|
Participants: participants,
|
|
CreatedAt: room.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: room.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}, nil
|
|
}
|
|
|
|
// ChatMessageResponse pour la réponse d'historique
|
|
type ChatMessageResponse struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ConversationID uuid.UUID `json:"conversation_id"`
|
|
SenderID uuid.UUID `json:"sender_id"`
|
|
Content string `json:"content"`
|
|
MessageType string `json:"message_type"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// GetRoomHistory récupère l'historique des messages d'une room
|
|
func (s *RoomService) GetRoomHistory(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]ChatMessageResponse, error) {
|
|
messages, err := s.messageRepo.GetConversationMessages(ctx, roomID, limit, offset)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) || err.Error() == "conversation not found" {
|
|
// Check if room exists first? Assuming Repo handles it or we could use GetRoom logic
|
|
// If messageRepo returns error on room not found
|
|
return nil, ErrRoomNotFound
|
|
}
|
|
s.logger.Error("failed to get room history",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
return nil, fmt.Errorf("failed to get room history: %w", err)
|
|
}
|
|
|
|
responses := make([]ChatMessageResponse, len(messages))
|
|
for i, msg := range messages {
|
|
responses[i] = ChatMessageResponse{
|
|
ID: msg.ID,
|
|
ConversationID: msg.ConversationID,
|
|
SenderID: msg.SenderID,
|
|
Content: msg.Content,
|
|
MessageType: msg.MessageType,
|
|
CreatedAt: msg.CreatedAt,
|
|
}
|
|
}
|
|
return responses, nil
|
|
}
|
|
|
|
// RoomHistoryWithCursorResult holds messages and next cursor for cursor-based pagination (v0.931).
|
|
type RoomHistoryWithCursorResult struct {
|
|
Messages []ChatMessageResponse
|
|
NextCursor string
|
|
}
|
|
|
|
// GetRoomHistoryWithCursor uses keyset pagination on (created_at, id) for consistent performance.
|
|
func (s *RoomService) GetRoomHistoryWithCursor(ctx context.Context, roomID uuid.UUID, limit int, cursor string) (*RoomHistoryWithCursorResult, error) {
|
|
result, err := s.messageRepo.GetConversationMessagesWithCursor(ctx, roomID, limit, cursor)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) || err.Error() == "conversation not found" {
|
|
return nil, ErrRoomNotFound
|
|
}
|
|
s.logger.Error("failed to get room history with cursor",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
return nil, fmt.Errorf("failed to get room history: %w", err)
|
|
}
|
|
responses := make([]ChatMessageResponse, len(result.Messages))
|
|
for i, msg := range result.Messages {
|
|
responses[i] = ChatMessageResponse{
|
|
ID: msg.ID,
|
|
ConversationID: msg.ConversationID,
|
|
SenderID: msg.SenderID,
|
|
Content: msg.Content,
|
|
MessageType: msg.MessageType,
|
|
CreatedAt: msg.CreatedAt,
|
|
}
|
|
}
|
|
return &RoomHistoryWithCursorResult{Messages: responses, NextCursor: result.NextCursor}, nil
|
|
}
|
|
|
|
// DeleteRoom supprime une room (soft delete)
|
|
// BE-API-010: Implement conversation delete endpoint
|
|
// Seul le créateur de la room ou un admin peut supprimer la room
|
|
func (s *RoomService) DeleteRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error {
|
|
// Vérifier que la room existe
|
|
room, err := s.roomRepo.GetByID(ctx, roomID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrRoomNotFound
|
|
}
|
|
return fmt.Errorf("failed to get room: %w", err)
|
|
}
|
|
|
|
// Vérifier que l'utilisateur est le créateur de la room
|
|
if room.CreatedBy != userID {
|
|
return fmt.Errorf("forbidden: only room creator can delete the room")
|
|
}
|
|
|
|
// Supprimer la room (soft delete via GORM)
|
|
if err := s.roomRepo.Delete(ctx, roomID); err != nil {
|
|
s.logger.Error("failed to delete room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
return fmt.Errorf("failed to delete room: %w", err)
|
|
}
|
|
|
|
s.logger.Info("room deleted successfully",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
return nil
|
|
}
|