using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nuuru.Server.Auth; using Nuuru.Server.DTOs; using Nuuru.Server.DTOs.Messaging; using Nuuru.Server.Extensions; using Nuuru.Server.Services; namespace Nuuru.Server.Controllers { [ApiController] [Route("api/messages/conversations")] [Authorize] public class ConversationController : ControllerBase { private readonly IConversationService _conversationService; private readonly IConversationVoiceService _conversationVoiceService; private readonly IUserBadgeService _userBadgeService; private readonly INotificationService _notificationService; private readonly ILogger _logger; public ConversationController( IConversationService conversationService, IConversationVoiceService conversationVoiceService, IUserBadgeService userBadgeService, INotificationService notificationService, ILogger logger) { _conversationService = conversationService; _conversationVoiceService = conversationVoiceService; _userBadgeService = userBadgeService; _notificationService = notificationService; _logger = logger; } /// /// Get user's conversations (inbox) /// [HttpGet] public async Task GetConversations([FromQuery] int page = 1, [FromQuery] int pageSize = 20) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } if (page < 1) { return BadRequest(new { error = "Page must be greater than 0" }); } if (pageSize < 1 || pageSize > 100) { return BadRequest(new { error = "Page size must be between 1 and 100" }); } var (conversations, totalCount) = await _conversationService.GetUserConversationsAsync(userId.Value, page, pageSize); // Batch-fetch display info for all participants var userIds = conversations .SelectMany(c => c.Participants.Where(p => p.User != null).Select(p => p.User!.Id)) .Concat(conversations.Where(c => c.Creator != null).Select(c => c.Creator!.Id)) .Concat(conversations.SelectMany(c => c.Messages.Where(m => m.Author != null).Select(m => m.Author!.Id))) .Distinct(); var displayInfoMap = await _userBadgeService.GetUsersDisplayInfoAsync(userIds); var conversationDtos = conversations.ToDto(userId.Value, displayInfoMap); return Ok(new PagedResult { Items = conversationDtos, Page = page, PageSize = pageSize, TotalCount = totalCount }); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving conversations"); return StatusCode(500, new { error = "Failed to retrieve conversations" }); } } /// /// Get unread conversation count for the current user /// [HttpGet("unread-count")] public async Task GetUnreadCount() { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var count = await _conversationService.GetUnreadConversationCountAsync(userId.Value); return Ok(new { count }); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving unread count"); return StatusCode(500, new { error = "Failed to retrieve unread count" }); } } /// /// Create a new conversation /// [HttpPost] [Authorize(Policy = Permissions.Messaging.SendMessage)] public async Task CreateConversation([FromBody] CreateConversationRequest request) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } // Check if group conversation permission is needed if (request.ParticipantIds.Count > 1) { var hasGroupPermission = User.HasPermission(Permissions.Messaging.CreateGroupConversation); if (!hasGroupPermission) { return StatusCode(403, new { error = "You don't have permission to create group conversations" }); } } var (conversation, createError) = await _conversationService.CreateConversationAsync( userId.Value, request.ParticipantIds, request.Title, request.Content); if (conversation == null) { return BadRequest(new { error = createError ?? "Failed to create conversation." }); } _logger.LogInformation("User {UserId} created conversation {ConversationId}", userId, conversation.Id); // Fetch display info var userIds = conversation.Participants.Where(p => p.User != null).Select(p => p.User!.Id) .Concat([conversation.Creator?.Id ?? Guid.Empty]) .Distinct() .Where(id => id != Guid.Empty); var displayInfoMap = await _userBadgeService.GetUsersDisplayInfoAsync(userIds); var conversationDto = conversation.ToDto(userId.Value, displayInfoMap); return CreatedAtAction(nameof(GetConversation), new { id = conversation.Id }, conversationDto); } catch (Exception ex) { _logger.LogError(ex, "Error creating conversation"); return StatusCode(500, new { error = "Failed to create conversation" }); } } /// /// Get a single conversation with details /// [HttpGet("{id:guid}")] public async Task GetConversation(Guid id) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var conversation = await _conversationService.GetConversationByIdAsync(id, userId.Value); if (conversation == null) { return NotFound(new { error = "Conversation not found" }); } await _notificationService.MarkReadByConversationAsync(userId.Value, id); // Fetch display info var userIds = conversation.Participants.Where(p => p.User != null).Select(p => p.User!.Id) .Concat([conversation.Creator?.Id ?? Guid.Empty]) .Concat(conversation.Messages.Where(m => m.Author != null).Select(m => m.Author!.Id)) .Distinct() .Where(id => id != Guid.Empty); var displayInfoMap = await _userBadgeService.GetUsersDisplayInfoAsync(userIds); var conversationDto = conversation.ToDto(userId.Value, displayInfoMap); return Ok(conversationDto); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to retrieve conversation" }); } } /// /// Get the current voice-room state for a conversation /// [HttpGet("{id:guid}/voice")] public async Task GetVoiceState(Guid id, CancellationToken cancellationToken) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var voiceState = await _conversationVoiceService.GetStateAsync(id, userId.Value, cancellationToken); if (voiceState == null) { return NotFound(new { error = "Conversation not found" }); } return Ok(voiceState); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving voice state for conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to retrieve voice state" }); } } /// /// Create a conversation-scoped LiveKit token for joining voice chat /// [HttpPost("{id:guid}/voice/token")] public async Task CreateVoiceToken(Guid id, CancellationToken cancellationToken) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } if (!_conversationVoiceService.IsEnabled) { return StatusCode(503, new { error = "Voice chat is not configured" }); } var voiceToken = await _conversationVoiceService.CreateJoinTokenAsync(id, userId.Value, cancellationToken); if (voiceToken == null) { return NotFound(new { error = "Conversation not found" }); } return Ok(voiceToken); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Voice chat is unavailable for conversation {ConversationId}", id); return StatusCode(503, new { error = "Voice chat is not configured" }); } catch (Exception ex) { _logger.LogError(ex, "Error creating voice token for conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to create voice token" }); } } /// /// Update whether the current participant is deafened in a conversation voice room /// [HttpPost("{id:guid}/voice/deafened")] public async Task SetVoiceDeafened(Guid id, [FromBody] UpdateConversationVoiceDeafenedRequest request, CancellationToken cancellationToken) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } if (!_conversationVoiceService.IsEnabled) { return StatusCode(503, new { error = "Voice chat is not configured" }); } var updated = await _conversationVoiceService.SetDeafenedAsync(id, userId.Value, request.Deafened, cancellationToken); if (updated == null) { return NotFound(new { error = "Conversation not found" }); } if (updated == false) { return Conflict(new { error = "You must be connected to voice chat to change deafened state" }); } return Ok(new { deafened = request.Deafened }); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Voice chat is unavailable for conversation {ConversationId}", id); return StatusCode(503, new { error = "Voice chat is not configured" }); } catch (Exception ex) { _logger.LogError(ex, "Error updating deafened state for conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to update deafened state" }); } } /// /// Mark conversation as read /// [HttpPost("{id:guid}/read")] public async Task MarkAsRead(Guid id) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var isParticipant = await _conversationService.IsParticipantAsync(id, userId.Value); if (!isParticipant) { return NotFound(new { error = "Conversation not found" }); } await _conversationService.MarkAsReadAsync(id, userId.Value); return Ok(new { success = true }); } catch (Exception ex) { _logger.LogError(ex, "Error marking conversation {ConversationId} as read", id); return StatusCode(500, new { error = "Failed to mark conversation as read" }); } } /// /// Mark all conversations as read /// [HttpPost("read-all")] public async Task MarkAllAsRead() { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } await _conversationService.MarkAllAsReadAsync(userId.Value); return Ok(new { success = true }); } catch (Exception ex) { _logger.LogError(ex, "Error marking all conversations as read"); return StatusCode(500, new { error = "Failed to mark all conversations as read" }); } } /// /// Add a participant to a conversation /// [HttpPost("{id:guid}/participants")] [Authorize(Policy = Permissions.Messaging.CreateGroupConversation)] public async Task AddParticipant(Guid id, [FromBody] AddParticipantRequest request) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var conversation = await _conversationService.GetConversationByIdAsync(id, userId.Value); if (conversation == null) { return BadRequest(new { error = "Failed to add participant. Check that the conversation exists and you're in it." }); } // Check permissions var isOwner = userId == conversation.CreatorId; var hasAddPermission = User.HasPermission(Permissions.Messaging.AddParticipant); if (!isOwner && !hasAddPermission) { return BadRequest(new { error = "Failed to add participant. You do not have permission to add participants to this conversation." }); } // Check if this is a re-add (user was previously a participant but left) var existingParticipant = conversation.Participants.FirstOrDefault(p => p.UserId == request.UserId); if (existingParticipant != null && existingParticipant.HasLeft && !hasAddPermission) { return BadRequest(new { error = "Only users with specific permission can re-add participants who have left." }); } var result = await _conversationService.AddParticipantAsync(id, userId.Value, request.UserId); if (!result) { return BadRequest(new { error = "Failed to add participant. Check that the user exists and isn't already in the conversation." }); } _logger.LogInformation("User {AddedByUserId} added user {NewUserId} to conversation {ConversationId}", userId, request.UserId, id); return Ok(new { success = true }); } catch (Exception ex) { _logger.LogError(ex, "Error adding participant to conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to add participant" }); } } /// /// Remove a participant from a conversation /// [HttpDelete("{id:guid}/participants/{removedUserId:guid}")] [Authorize(Policy = Permissions.Messaging.CreateGroupConversation)] public async Task RemoveParticipant(Guid id, Guid removedUserId) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var conversation = await _conversationService.GetConversationByIdAsync(id, userId.Value); if (conversation == null) { return BadRequest(new { error = "Failed to remove participant. Check that the conversation exists and you're in it." }); } // Check permissions var isOwner = userId == conversation.CreatorId; if (!isOwner && !User.HasPermission(Permissions.Messaging.RemoveParticipant)) { return BadRequest(new { error = "Failed to remove participant. You do not have permission to remove participants from this conversation." }); } var result = await _conversationService.RemoveParticipantAsync(id, userId.Value, removedUserId); if (!result) { return BadRequest(new { error = "Failed to remove participant. Check that the user exists and is present in this conversation." }); } _logger.LogInformation("User {RemovedByUserId} removed user {RemovedUserId} from conversation {ConversationId}", userId, removedUserId, id); return NoContent(); } catch (Exception ex) { _logger.LogError(ex, "Error removing participant from conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to remove participant" }); } } /// /// Leave a conversation /// [HttpDelete("{id:guid}/leave")] public async Task LeaveConversation(Guid id) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var result = await _conversationService.LeaveConversationAsync(id, userId.Value); if (!result) { return BadRequest(new { error = "Failed to leave conversation" }); } _logger.LogInformation("User {UserId} left conversation {ConversationId}", userId, id); return NoContent(); } catch (Exception ex) { _logger.LogError(ex, "Error leaving conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to leave conversation" }); } } /// /// Lock or unlock a conversation /// [HttpPost("{id:guid}/lock")] [Authorize(Policy = Permissions.Messaging.LockConversation)] public async Task LockConversation(Guid id, [FromBody] LockConversationRequest request) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } // Verify user is a participant var isParticipant = await _conversationService.IsParticipantAsync(id, userId.Value); if (!isParticipant) { return NotFound(new { error = "Conversation not found" }); } var result = await _conversationService.LockConversationAsync(id, request.Locked); if (!result) { return StatusCode(500, new { error = "Failed to update conversation" }); } _logger.LogInformation("Conversation {ConversationId} {Action} by user {UserId}", id, request.Locked ? "locked" : "unlocked", userId); return Ok(new { locked = request.Locked }); } catch (Exception ex) { _logger.LogError(ex, "Error locking conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to update conversation" }); } } /// /// Delete a conversation /// [HttpDelete("{id:guid}")] [Authorize(Policy = Permissions.Messaging.DeleteConversation)] public async Task DeleteConversation(Guid id) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } // Verify user is a participant var isParticipant = await _conversationService.IsParticipantAsync(id, userId.Value); if (!isParticipant) { return NotFound(new { error = "Conversation not found" }); } var result = await _conversationService.DeleteConversationAsync(id); if (!result) { return StatusCode(500, new { error = "Failed to delete conversation" }); } _logger.LogInformation("Conversation {ConversationId} deleted by user {UserId}", id, userId); return NoContent(); } catch (Exception ex) { _logger.LogError(ex, "Error deleting conversation {ConversationId}", id); return StatusCode(500, new { error = "Failed to delete conversation" }); } } } }