using Azure; using Azure.Communication; using Azure.Communication.CallAutomation; using Azure.Communication.Identity; using Azure.Communication.Rooms; using EnotaryoPH.Data; using EnotaryoPH.Data.Entities; namespace EnotaryoPH.Web.Common.Services { public class VideoConferenceService : IVideoConferenceService { private const int VideoConferenceExpirationInHours = 2; private readonly CallAutomationClient _callAutomationClient; private readonly CommunicationIdentityClient _communicationIdentityClient; private readonly IConfiguration _configuration; private readonly NotaryoDBContext _dbContext; private readonly IEventService _eventService; private readonly RoomsClient _roomsClient; public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService) { _dbContext = dbContext; _communicationIdentityClient = communicationIdentityClient; _roomsClient = roomsClient; _callAutomationClient = callAutomationClient; _configuration = configuration; _eventService = eventService; } public async Task ApproveTransactionAsync(Guid transaction_UID) { var transaction = _dbContext.Transactions .Include(t => t.TransactionSignatories) .ThenInclude(ts => ts.User) .Include(t => t.Schedule) .Include(t => t.Lawyer) .ThenInclude(l => l.User) .Include(t => t.Principal) .FirstOrDefault(t => t.Transaction_UID == transaction_UID); NoDataException.ThrowIfNull(transaction, nameof(Transaction), transaction_UID); transaction.Status = nameof(TransactionState.Approved); transaction.TransactionSignatories.ForEach(ts => ts.Status = nameof(SignatoryStatus.Approved)); transaction.Schedule.Status = nameof(VideoConferenceStatus.Completed); _dbContext.Update(transaction); _dbContext.SaveChanges(); await Task.WhenAll( _eventService.LogAsync(NotaryoEvent.TransactionApproved, transaction_UID), StopRecordingAsync(transaction.Schedule) ); } public bool CanStart(Guid transaction_UID) { if (transaction_UID == Guid.Empty) { return false; } var transactionEntity = _dbContext.Transactions .AsNoTracking() .Include(t => t.TransactionSignatories) .FirstOrDefault(t => t.Transaction_UID == transaction_UID); if (transactionEntity == null) { return false; } var isReadyForVideoCall = transactionEntity.TransactionSignatories.All(signatory => signatory.Status == nameof(SignatoryStatus.FaceMatch)); var isAcceptedByLawyer = transactionEntity.Status == nameof(TransactionState.Accepted) && transactionEntity.LawyerID > 0; return isReadyForVideoCall && isAcceptedByLawyer; } public Guid GetUIDByTransactionUID(Guid transaction_UID) { var transactionEntity = _dbContext.Transactions .AsNoTracking() .FirstOrDefault(t => t.Transaction_UID == transaction_UID); if (transactionEntity == null) { return Guid.Empty; } var schedule = _dbContext.LawyerVideoConferenceSchedules.AsNoTracking().FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); return schedule == null ? Guid.Empty : schedule.LawyerVideoConferenceSchedule_UID; } public bool HasExpired(Guid transaction_UID) { var transactionEntity = _dbContext.Transactions .Include(t => t.TransactionSignatories) .FirstOrDefault(t => t.Transaction_UID == transaction_UID); if (transactionEntity == null) { return false; } var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); if (schedule == null) { return false; } if (schedule.Status == nameof(VideoConferenceStatus.Expired)) { return true; } if ((DateTime.UtcNow - schedule.MeetingDate).TotalHours > VideoConferenceExpirationInHours) { if (!schedule.Status?.IsInList(VideoConferenceStatus.Abandoned, VideoConferenceStatus.Completed) ?? false) { schedule.Status = nameof(VideoConferenceStatus.Expired); _dbContext.Update(schedule); _dbContext.SaveChanges(); } return true; } return false; } public async Task StartAsync(Guid transaction_UID) { if (!CanStart(transaction_UID)) { throw new ArgumentException("Transaction is not ready for video conference."); } var schedule = GetOrCreateNewSchedule(transaction_UID); if (schedule.Status == nameof(VideoConferenceStatus.New)) { await CreateMeetingRoomAsync(schedule); _dbContext.UpdateOrCreate(schedule); _dbContext.SaveChanges(); } return schedule.LawyerVideoConferenceSchedule_UID; } public async Task StartRecordingAsync(Guid transaction_UID, string serverCallID) { var transactionEntity = _dbContext.Transactions .Include(t => t.TransactionSignatories) .Single(t => t.Transaction_UID == transaction_UID); NoDataException.ThrowIfNull(transactionEntity, nameof(Transaction), transaction_UID); var schedule = _dbContext.LawyerVideoConferenceSchedules .FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); NoDataException.ThrowIfNull(schedule, nameof(LawyerVideoConferenceSchedule), transactionEntity.TransactionID, FullName.Of(transactionEntity.TransactionID)); if (string.IsNullOrEmpty(schedule.RecordingID)) { schedule.RecordingID = await StartRecordingAsync(serverCallID); schedule.ServerCallID = serverCallID; _dbContext.Update(schedule); _dbContext.SaveChanges(); } } private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule) { var roomParticipants = new List(); foreach (var participant in schedule.LawyerVideoConferenceParticipants.ToSafeList()) { var attendee = await _communicationIdentityClient.CreateUserAsync(); participant.MeetingRoomTokenID = await GetTokenResponseAsync(attendee); participant.MeetingRoomUserID = attendee.Value.Id; roomParticipants.Add(new RoomParticipant(attendee) { Role = ParticipantRole.Attendee }); } var presenter = await _communicationIdentityClient.CreateUserAsync(); schedule.MeetingRoomTokenID = await GetTokenResponseAsync(presenter); schedule.MeetingRoomUserID = presenter.Value.Id; roomParticipants.Add(new RoomParticipant(presenter) { Role = ParticipantRole.Presenter }); CommunicationRoom room = await _roomsClient.CreateRoomAsync(DateTime.Now, DateTime.Now.AddHours(2), roomParticipants); schedule.MeetingRoomID = room.Id; schedule.Status = nameof(VideoConferenceStatus.InProgress); } private LawyerVideoConferenceSchedule GetOrCreateNewSchedule(Guid transaction_UID) { var transactionEntity = _dbContext.Transactions .Include(t => t.TransactionSignatories) .Single(t => t.Transaction_UID == transaction_UID); var schedule = _dbContext.LawyerVideoConferenceSchedules.FirstOrDefault(sched => sched.TransactionID == transactionEntity.TransactionID); if (schedule != null) { return schedule; } schedule = new LawyerVideoConferenceSchedule { LawyerVideoConferenceSchedule_UID = Guid.CreateVersion7(DateTime.UtcNow), CreatedOn = DateTime.UtcNow, LawyerID = transactionEntity.LawyerID.GetValueOrDefault(), TransactionID = transactionEntity.TransactionID, Status = nameof(VideoConferenceStatus.New) }; var participants = transactionEntity.TransactionSignatories.ConvertAll(signatory => new LawyerVideoConferenceParticipant { CreatedOn = DateTime.UtcNow, Status = nameof(VideoConferenceStatus.New), LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), ParticipantID = signatory.UserID, }); participants.Add(new LawyerVideoConferenceParticipant { CreatedOn = DateTime.UtcNow, Status = nameof(VideoConferenceStatus.New), LawyerVideoConferenceParticipant_UID = Guid.CreateVersion7(DateTime.UtcNow), ParticipantID = transactionEntity.PrincipalID, }); schedule.MeetingDate = DateTime.UtcNow; schedule.LawyerVideoConferenceParticipants = participants.ToList(); return schedule; } private async Task GetTokenResponseAsync(Response user) { var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP }); return tokenResponse.Value.Token; } private async Task StartRecordingAsync(string serverCallID) { ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID); CallLocator callLocator = new ServerCallLocator(serverCallID); var uri = _configuration.GetValue("UriRecordingBlobContainer") ?? string.Empty; var recordingResult = await _callAutomationClient .GetCallRecording().StartAsync(new StartRecordingOptions(callLocator) { RecordingContent = RecordingContent.AudioVideo, RecordingStorage = RecordingStorage.CreateAzureBlobContainerRecordingStorage(new Uri(uri)), RecordingFormat = RecordingFormat.Mp4 }); return recordingResult.Value.RecordingId; } private async Task StopRecordingAsync(LawyerVideoConferenceSchedule schedule) { if (string.IsNullOrEmpty(schedule.ServerCallID)) { Console.WriteLine("ServerCallID is not set for this transaction."); return; } if (string.IsNullOrEmpty(schedule.RecordingID)) { Console.WriteLine("Recording ID is not set for this transaction."); return; } await _callAutomationClient.GetCallRecording().StopAsync(schedule.RecordingID); } } }