using DysonNetwork.Sphere.Chat.Realtime; using Livekit.Server.Sdk.Dotnet; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Swashbuckle.AspNetCore.Annotations; namespace DysonNetwork.Sphere.Chat; public class RealtimeChatConfiguration { public string Endpoint { get; set; } = null!; } [ApiController] [Route("/chat/realtime")] public class RealtimeCallController( IConfiguration configuration, AppDatabase db, ChatService cs, IRealtimeService realtime ) : ControllerBase { private readonly RealtimeChatConfiguration _config = configuration.GetSection("RealtimeChat").Get()!; /// /// This endpoint is especially designed for livekit webhooks, /// for update the call participates and more. /// Learn more at: https://docs.livekit.io/home/server/webhooks/ /// [HttpPost("webhook")] [SwaggerIgnore] public async Task WebhookReceiver() { using var reader = new StreamReader(Request.Body); var postData = await reader.ReadToEndAsync(); var authHeader = Request.Headers.Authorization.ToString(); await realtime.ReceiveWebhook(postData, authHeader); return Ok(); } [HttpGet("{roomId:guid}")] [Authorize] public async Task> GetOngoingCall(Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); var member = await db.ChatMembers .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You need to be a member to view call status."); var ongoingCall = await db.ChatRealtimeCall .Where(c => c.RoomId == roomId) .Where(c => c.EndedAt == null) .Include(c => c.Room) .Include(c => c.Sender) .ThenInclude(c => c.Account) .ThenInclude(c => c.Profile) .FirstOrDefaultAsync(); if (ongoingCall is null) return NotFound(); return Ok(ongoingCall); } [HttpGet("{roomId:guid}/join")] [Authorize] public async Task> JoinCall(Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); // Check if the user is a member of the chat room var member = await db.ChatMembers .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You need to be a member to join a call."); // Get ongoing call var ongoingCall = await cs.GetCallOngoingAsync(roomId); if (ongoingCall is null) return NotFound("There is no ongoing call in this room."); // Check if session ID exists if (string.IsNullOrEmpty(ongoingCall.SessionId)) return BadRequest("Call session is not properly configured."); var isAdmin = member.Role >= ChatMemberRole.Moderator; var userToken = realtime.GetUserToken(currentUser, ongoingCall.SessionId, isAdmin); // Get LiveKit endpoint from configuration var endpoint = _config.Endpoint ?? throw new InvalidOperationException("LiveKit endpoint configuration is missing"); // Inject the ChatRoomService var chatRoomService = HttpContext.RequestServices.GetRequiredService(); // Get current participants from the LiveKit service var participants = new List(); if (realtime is LivekitRealtimeService livekitService) { var roomParticipants = await livekitService.GetRoomParticipantsAsync(ongoingCall.SessionId); participants = new List(); foreach (var p in roomParticipants) { var participant = new CallParticipant { Identity = p.Identity, Name = p.Name, AccountId = p.AccountId, JoinedAt = p.JoinedAt }; // Fetch the ChatMember profile if we have an account ID if (p.AccountId.HasValue) participant.Profile = await chatRoomService.GetChannelMember(p.AccountId.Value, roomId); participants.Add(participant); } } // Create the response model var response = new JoinCallResponse { Provider = realtime.ProviderName, Endpoint = endpoint, Token = userToken, CallId = ongoingCall.Id, RoomName = ongoingCall.SessionId, IsAdmin = isAdmin, Participants = participants }; return Ok(response); } [HttpPost("{roomId:guid}")] [Authorize] public async Task> StartCall(Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); var member = await db.ChatMembers .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) .Include(m => m.ChatRoom) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You need to be a normal member to start a call."); var ongoingCall = await cs.GetCallOngoingAsync(roomId); if (ongoingCall is not null) return StatusCode(423, "There is already an ongoing call inside the chatroom."); var call = await cs.CreateCallAsync(member.ChatRoom, member); return Ok(call); } [HttpDelete("{roomId:guid}")] [Authorize] public async Task> EndCall(Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); var member = await db.ChatMembers .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You need to be a normal member to end a call."); try { await cs.EndCallAsync(roomId, member); return NoContent(); } catch (Exception exception) { return BadRequest(exception.Message); } } } // Response model for joining a call public class JoinCallResponse { /// /// The service provider name (e.g., "LiveKit") /// public string Provider { get; set; } = null!; /// /// The LiveKit server endpoint /// public string Endpoint { get; set; } = null!; /// /// Authentication token for the user /// public string Token { get; set; } = null!; /// /// The call identifier /// public Guid CallId { get; set; } /// /// The room name in LiveKit /// public string RoomName { get; set; } = null!; /// /// Whether the user is the admin of the call /// public bool IsAdmin { get; set; } /// /// Current participants in the call /// public List Participants { get; set; } = new(); } /// /// Represents a participant in a real-time call /// public class CallParticipant { /// /// The participant's identity (username) /// public string Identity { get; set; } = null!; /// /// The participant's display name /// public string Name { get; set; } = null!; /// /// The participant's account ID if available /// public Guid? AccountId { get; set; } /// /// The participant's profile in the chat /// public ChatMember? Profile { get; set; } /// /// When the participant joined the call /// public DateTime JoinedAt { get; set; } }