2025-04-10 08:31:36 +01:00

285 lines
13 KiB
C#

using System.Security.Cryptography;
using Azure;
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Communication.Identity;
using Azure.Communication.Rooms;
using Coravel.Queuing.Interfaces;
using EnotaryoPH.Data;
using EnotaryoPH.Data.Entities;
using EnotaryoPH.Web.Common.Jobs;
using EnotaryoPH.Web.Common.Jobs.Models;
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 IPasswordService _passwordService;
private readonly IQueue _queue;
private readonly RoomsClient _roomsClient;
public VideoConferenceService(NotaryoDBContext dbContext, CommunicationIdentityClient communicationIdentityClient, RoomsClient roomsClient, CallAutomationClient callAutomationClient, IConfiguration configuration, IEventService eventService, IPasswordService passwordService, IQueue queue)
{
_dbContext = dbContext;
_communicationIdentityClient = communicationIdentityClient;
_roomsClient = roomsClient;
_callAutomationClient = callAutomationClient;
_configuration = configuration;
_eventService = eventService;
_passwordService = passwordService;
_queue = queue;
}
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<Guid> 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);
await CreateAndEmailOTPAsync(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 CreateAndEmailOTPAsync(LawyerVideoConferenceSchedule schedule)
{
var participantOTP = new Dictionary<Guid, string>();
const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
foreach (var participant in schedule.LawyerVideoConferenceParticipants)
{
var otp = RandomNumberGenerator.GetString(validChars, 5);
participant.OTPHash = _passwordService.HashPassword(otp);
participantOTP.Add(participant.LawyerVideoConferenceParticipant_UID.GetValueOrDefault(), otp);
}
_queue.QueueInvocableWithPayload<OneTimePasswordInvocable, OTPEmailModel>(new OTPEmailModel
{
ParticipantOTP = participantOTP,
Transaction_UID = schedule.Transaction.Transaction_UID.GetValueOrDefault()
});
}
private async Task CreateMeetingRoomAsync(LawyerVideoConferenceSchedule schedule)
{
var roomParticipants = new List<RoomParticipant>();
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
.Include(sched => sched.Transaction)
.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();
schedule.Transaction = transactionEntity;
return schedule;
}
private async Task<string> GetTokenResponseAsync(Response<CommunicationUserIdentifier> user)
{
var tokenResponse = await _communicationIdentityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
return tokenResponse.Value.Token;
}
private async Task<string> StartRecordingAsync(string serverCallID)
{
ArgumentException.ThrowIfNullOrWhiteSpace(serverCallID);
CallLocator callLocator = new ServerCallLocator(serverCallID);
var uri = _configuration.GetValue<string>("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);
}
}
}