260 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using DysonNetwork.Shared.Models;
 | 
						|
using DysonNetwork.Shared.Proto;
 | 
						|
using DysonNetwork.Sphere.Chat.Realtime;
 | 
						|
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("/api/chat/realtime")]
 | 
						|
public class RealtimeCallController(
 | 
						|
    IConfiguration configuration,
 | 
						|
    AppDatabase db,
 | 
						|
    ChatService cs,
 | 
						|
    ChatRoomService crs,
 | 
						|
    IRealtimeService realtime
 | 
						|
) : ControllerBase
 | 
						|
{
 | 
						|
    private readonly RealtimeChatConfiguration _config =
 | 
						|
        configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// 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/
 | 
						|
    /// </summary>
 | 
						|
    [HttpPost("webhook")]
 | 
						|
    [SwaggerIgnore]
 | 
						|
    public async Task<IActionResult> 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<ActionResult<SnRealtimeCall>> GetOngoingCall(Guid roomId)
 | 
						|
    {
 | 
						|
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
						|
 | 
						|
        var accountId = Guid.Parse(currentUser.Id);
 | 
						|
        var member = await db.ChatMembers
 | 
						|
            .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
 | 
						|
            .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)
 | 
						|
            .FirstOrDefaultAsync();
 | 
						|
        if (ongoingCall is null) return NotFound();
 | 
						|
        ongoingCall.Sender = await crs.LoadMemberAccount(ongoingCall.Sender);
 | 
						|
        return Ok(ongoingCall);
 | 
						|
    }
 | 
						|
 | 
						|
    [HttpGet("{roomId:guid}/join")]
 | 
						|
    [Authorize]
 | 
						|
    public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId)
 | 
						|
    {
 | 
						|
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
						|
 | 
						|
        // Check if the user is a member of the chat room
 | 
						|
        var accountId = Guid.Parse(currentUser.Id);
 | 
						|
        var member = await db.ChatMembers
 | 
						|
            .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
 | 
						|
            .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<ChatRoomService>();
 | 
						|
 | 
						|
        // Get current participants from the LiveKit service
 | 
						|
        var participants = new List<CallParticipant>();
 | 
						|
        if (realtime is LiveKitRealtimeService livekitService)
 | 
						|
        {
 | 
						|
            var roomParticipants = await livekitService.GetRoomParticipantsAsync(ongoingCall.SessionId);
 | 
						|
            participants = [];
 | 
						|
            
 | 
						|
            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.GetRoomMember(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<ActionResult<SnRealtimeCall>> StartCall(Guid roomId)
 | 
						|
    {
 | 
						|
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
						|
 | 
						|
        var accountId = Guid.Parse(currentUser.Id);
 | 
						|
        var member = await db.ChatMembers
 | 
						|
            .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
 | 
						|
            .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<ActionResult<SnRealtimeCall>> EndCall(Guid roomId)
 | 
						|
    {
 | 
						|
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
						|
 | 
						|
        var accountId = Guid.Parse(currentUser.Id);
 | 
						|
        var member = await db.ChatMembers
 | 
						|
            .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
 | 
						|
            .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
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// The service provider name (e.g., "LiveKit")
 | 
						|
    /// </summary>
 | 
						|
    public string Provider { get; set; } = null!;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// The LiveKit server endpoint
 | 
						|
    /// </summary>
 | 
						|
    public string Endpoint { get; set; } = null!;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Authentication token for the user
 | 
						|
    /// </summary>
 | 
						|
    public string Token { get; set; } = null!;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// The call identifier
 | 
						|
    /// </summary>
 | 
						|
    public Guid CallId { get; set; }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// The room name in LiveKit
 | 
						|
    /// </summary>
 | 
						|
    public string RoomName { get; set; } = null!;
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Whether the user is the admin of the call
 | 
						|
    /// </summary>
 | 
						|
    public bool IsAdmin { get; set; }
 | 
						|
    
 | 
						|
    /// <summary>
 | 
						|
    /// Current participants in the call
 | 
						|
    /// </summary>
 | 
						|
    public List<CallParticipant> Participants { get; set; } = new();
 | 
						|
}
 | 
						|
 | 
						|
/// <summary>
 | 
						|
/// Represents a participant in a real-time call
 | 
						|
/// </summary>
 | 
						|
public class CallParticipant
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// The participant's identity (username)
 | 
						|
    /// </summary>
 | 
						|
    public string Identity { get; set; } = null!;
 | 
						|
    
 | 
						|
    /// <summary>
 | 
						|
    /// The participant's display name
 | 
						|
    /// </summary>
 | 
						|
    public string Name { get; set; } = null!;
 | 
						|
    
 | 
						|
    /// <summary>
 | 
						|
    /// The participant's account ID if available
 | 
						|
    /// </summary>
 | 
						|
    public Guid? AccountId { get; set; }
 | 
						|
    
 | 
						|
    /// <summary>
 | 
						|
    /// The participant's profile in the chat
 | 
						|
    /// </summary>
 | 
						|
    public SnChatMember? Profile { get; set; }
 | 
						|
    
 | 
						|
    /// <summary>
 | 
						|
    /// When the participant joined the call
 | 
						|
    /// </summary>
 | 
						|
    public DateTime JoinedAt { get; set; }
 | 
						|
}
 |