433 lines
14 KiB
Go
433 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// RoomServiceInterface defines the interface for room service operations
|
|
type RoomServiceInterface interface {
|
|
CreateRoom(ctx context.Context, userID uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error)
|
|
GetUserRooms(ctx context.Context, userID uuid.UUID) ([]*services.RoomResponse, error)
|
|
GetRoom(ctx context.Context, roomID uuid.UUID) (*services.RoomResponse, error)
|
|
UpdateRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req services.UpdateRoomRequest) (*services.RoomResponse, error) // BE-API-012: Update room method
|
|
AddMember(ctx context.Context, roomID, userID uuid.UUID) error
|
|
RemoveMember(ctx context.Context, roomID, userID uuid.UUID) error // BE-API-011: Remove member method
|
|
GetRoomHistory(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]services.ChatMessageResponse, error)
|
|
GetRoomHistoryWithCursor(ctx context.Context, roomID uuid.UUID, limit int, cursor string) (*services.RoomHistoryWithCursorResult, error) // v0.931: cursor pagination
|
|
DeleteRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error // BE-API-010: Delete room method
|
|
}
|
|
|
|
// RoomHandler gère les opérations sur les rooms (conversations)
|
|
type RoomHandler struct {
|
|
roomService RoomServiceInterface
|
|
logger *zap.Logger
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewRoomHandler crée une nouvelle instance de RoomHandler
|
|
func NewRoomHandler(roomService RoomServiceInterface, logger *zap.Logger) *RoomHandler {
|
|
return &RoomHandler{
|
|
roomService: roomService,
|
|
logger: logger,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// CreateRoom gère la création d'une nouvelle room
|
|
// POST /api/v1/conversations
|
|
func (h *RoomHandler) CreateRoom(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur du contexte
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Convertir userID en uuid.UUID
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type in context"})
|
|
return
|
|
}
|
|
|
|
// Parser la requête
|
|
var req services.CreateRoomRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Valider le type de room si non spécifié
|
|
if req.Type == "" {
|
|
req.Type = "public"
|
|
}
|
|
|
|
// Créer la room
|
|
room, err := h.roomService.CreateRoom(c.Request.Context(), userID, req)
|
|
if err != nil {
|
|
h.logger.Error("failed to create room",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("room_name", req.Name))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create conversation"})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("room created successfully",
|
|
zap.String("room_id", room.ID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("room_name", req.Name))
|
|
|
|
RespondSuccess(c, http.StatusCreated, room)
|
|
}
|
|
|
|
// GetUserRooms récupère toutes les rooms d'un utilisateur
|
|
// GET /api/v1/conversations
|
|
func (h *RoomHandler) GetUserRooms(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur du contexte
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Convertir userID en uuid.UUID
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type in context"})
|
|
return
|
|
}
|
|
|
|
// Récupérer les rooms
|
|
rooms, err := h.roomService.GetUserRooms(c.Request.Context(), userID)
|
|
if err != nil {
|
|
h.logger.Error("failed to get user rooms",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch conversations"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"conversations": rooms,
|
|
"total": len(rooms),
|
|
})
|
|
}
|
|
|
|
// GetRoom récupère une room par son ID
|
|
// GET /api/v1/conversations/:id
|
|
func (h *RoomHandler) GetRoom(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid room ID"})
|
|
return
|
|
}
|
|
|
|
// Récupérer la room
|
|
room, err := h.roomService.GetRoom(c.Request.Context(), roomID)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrRoomNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Conversation not found"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to get room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get conversation"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, room)
|
|
}
|
|
|
|
// UpdateRoom met à jour une room (conversation)
|
|
// PUT /api/v1/conversations/:id
|
|
// BE-API-012: Implement conversation update endpoint
|
|
func (h *RoomHandler) UpdateRoom(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid room id"))
|
|
return
|
|
}
|
|
|
|
// Récupérer l'ID utilisateur du contexte
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Parser la requête
|
|
var req services.UpdateRoomRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Mettre à jour la room
|
|
room, err := h.roomService.UpdateRoom(c.Request.Context(), roomID, userID, req)
|
|
if err != nil {
|
|
if err.Error() == "room not found" || errors.Is(err, services.ErrRoomNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("conversation"))
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only room creator can update the room" {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("only room creator can update the room"))
|
|
return
|
|
}
|
|
h.logger.Error("failed to update room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update conversation", err))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("room updated successfully",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, room)
|
|
}
|
|
|
|
// AddMemberRequest représente une requête pour ajouter un membre à une room
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type AddMemberRequest struct {
|
|
UserID uuid.UUID `json:"user_id" binding:"required" validate:"required"` // Changed to UUID
|
|
}
|
|
|
|
// AddMember ajoute un membre à une room
|
|
// POST /api/v1/conversations/:id/members
|
|
func (h *RoomHandler) AddMember(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid room ID"})
|
|
return
|
|
}
|
|
|
|
// Parser la requête
|
|
var req AddMemberRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Ajouter le membre
|
|
if err := h.roomService.AddMember(c.Request.Context(), roomID, req.UserID); err != nil {
|
|
h.logger.Error("failed to add member to room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", req.UserID.String()))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add member"})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("member added to room",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", req.UserID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Member added successfully"})
|
|
}
|
|
|
|
// GetRoomHistory récupère l'historique des messages d'une room
|
|
// GET /api/v1/conversations/:id/history
|
|
// v0.931: Supports cursor-based pagination via ?cursor=xxx&limit=20. Falls back to offset when cursor not provided.
|
|
func (h *RoomHandler) GetRoomHistory(c *gin.Context) {
|
|
conversationIDStr := c.Param("id")
|
|
conversationID, err := uuid.Parse(conversationIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid conversation ID"})
|
|
return
|
|
}
|
|
|
|
limit := c.DefaultQuery("limit", "50")
|
|
limitInt, err := strconv.Atoi(limit)
|
|
if err != nil || limitInt <= 0 {
|
|
limitInt = 50
|
|
}
|
|
if limitInt > 100 {
|
|
limitInt = 100
|
|
}
|
|
cursor := c.Query("cursor")
|
|
|
|
if cursor != "" {
|
|
result, err := h.roomService.GetRoomHistoryWithCursor(c.Request.Context(), conversationID, limitInt, cursor)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrRoomNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Conversation not found"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to get room history",
|
|
zap.Error(err),
|
|
zap.String("conversation_id", conversationID.String()))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get conversation history"})
|
|
return
|
|
}
|
|
resp := gin.H{"messages": result.Messages}
|
|
if result.NextCursor != "" {
|
|
resp["next_cursor"] = result.NextCursor
|
|
}
|
|
RespondSuccess(c, http.StatusOK, resp)
|
|
return
|
|
}
|
|
|
|
offset := c.DefaultQuery("offset", "0")
|
|
offsetInt, err := strconv.Atoi(offset)
|
|
if err != nil || offsetInt < 0 {
|
|
offsetInt = 0
|
|
}
|
|
|
|
messages, err := h.roomService.GetRoomHistory(c.Request.Context(), conversationID, limitInt, offsetInt)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrRoomNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Conversation not found"})
|
|
return
|
|
}
|
|
h.logger.Error("failed to get room history",
|
|
zap.Error(err),
|
|
zap.String("conversation_id", conversationID.String()))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get conversation history"})
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"messages": messages})
|
|
}
|
|
|
|
// DeleteRoom supprime une room (conversation)
|
|
// DELETE /api/v1/conversations/:id
|
|
// BE-API-010: Implement conversation delete endpoint
|
|
func (h *RoomHandler) DeleteRoom(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid room id"))
|
|
return
|
|
}
|
|
|
|
// Récupérer l'ID utilisateur du contexte
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Supprimer la room
|
|
if err := h.roomService.DeleteRoom(c.Request.Context(), roomID, userID); err != nil {
|
|
if err.Error() == "room not found" || errors.Is(err, services.ErrRoomNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("conversation"))
|
|
return
|
|
}
|
|
if err.Error() == "forbidden: only room creator can delete the room" {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("only room creator can delete the room"))
|
|
return
|
|
}
|
|
h.logger.Error("failed to delete room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete conversation", err))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("room deleted successfully",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Conversation deleted successfully"})
|
|
}
|
|
|
|
// AddParticipant ajoute un participant à une conversation
|
|
// POST /api/v1/conversations/:id/participants
|
|
// BE-API-011: Implement conversation participants endpoints
|
|
func (h *RoomHandler) AddParticipant(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid room id"))
|
|
return
|
|
}
|
|
|
|
// Parser la requête
|
|
var req AddMemberRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Ajouter le participant (utilise la même logique que AddMember)
|
|
if err := h.roomService.AddMember(c.Request.Context(), roomID, req.UserID); err != nil {
|
|
if errors.Is(err, services.ErrRoomNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("conversation"))
|
|
return
|
|
}
|
|
h.logger.Error("failed to add participant to room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", req.UserID.String()))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add participant", err))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("participant added to room",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", req.UserID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Participant added successfully"})
|
|
}
|
|
|
|
// RemoveParticipant retire un participant d'une conversation
|
|
// DELETE /api/v1/conversations/:id/participants/:userId
|
|
// BE-API-011: Implement conversation participants endpoints
|
|
func (h *RoomHandler) RemoveParticipant(c *gin.Context) {
|
|
// Récupérer l'ID de la room depuis l'URL
|
|
roomIDStr := c.Param("id")
|
|
roomID, err := uuid.Parse(roomIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid room id"))
|
|
return
|
|
}
|
|
|
|
// Récupérer l'ID de l'utilisateur depuis l'URL
|
|
userIDStr := c.Param("userId")
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
|
return
|
|
}
|
|
|
|
// Retirer le participant
|
|
if err := h.roomService.RemoveMember(c.Request.Context(), roomID, userID); err != nil {
|
|
if errors.Is(err, services.ErrRoomNotFound) {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("conversation"))
|
|
return
|
|
}
|
|
h.logger.Error("failed to remove participant from room",
|
|
zap.Error(err),
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove participant", err))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("participant removed from room",
|
|
zap.String("room_id", roomID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Participant removed successfully"})
|
|
}
|