veza/veza-backend-api/internal/services/room_service.go
senke 1318a53a64
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
chore(release): v0.931 — Cursor (cursor-based pagination, performance baseline)
2026-03-02 12:35:49 +01:00

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
}