From 9c57d6d284b5343a74d8b667418e940bfd7aaa33 Mon Sep 17 00:00:00 2001 From: Tommy Date: Mon, 19 Sep 2022 13:48:41 +0200 Subject: [PATCH] Remove TAPI address notification properly, if all connections of a user have disconnected --- .../CallSubscriberConnection.cs | 14 +++ .../ITelephonyConnector.cs | 3 +- src/PhoneBox.Server/SignalR/TelephonyHub.cs | 38 ++++++- .../WebHook/WebHookConnector.cs | 3 +- src/PhoneBox.TapiService/TapiConnector.cs | 104 ++++++++++++++---- 5 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 src/PhoneBox.Abstractions/CallSubscriberConnection.cs diff --git a/src/PhoneBox.Abstractions/CallSubscriberConnection.cs b/src/PhoneBox.Abstractions/CallSubscriberConnection.cs new file mode 100644 index 0000000..2420990 --- /dev/null +++ b/src/PhoneBox.Abstractions/CallSubscriberConnection.cs @@ -0,0 +1,14 @@ +namespace PhoneBox.Abstractions +{ + public readonly struct CallSubscriberConnection + { + public string ConnectionId { get; } + public CallSubscriber Subscriber { get; } + + public CallSubscriberConnection(string connectionId, CallSubscriber subscriber) + { + this.ConnectionId = connectionId; + this.Subscriber = subscriber; + } + } +} \ No newline at end of file diff --git a/src/PhoneBox.Abstractions/ITelephonyConnector.cs b/src/PhoneBox.Abstractions/ITelephonyConnector.cs index a165932..ad9e0d5 100644 --- a/src/PhoneBox.Abstractions/ITelephonyConnector.cs +++ b/src/PhoneBox.Abstractions/ITelephonyConnector.cs @@ -2,6 +2,7 @@ { public interface ITelephonyConnector { - void Subscribe(CallSubscriber subscriber); + void Subscribe(CallSubscriberConnection connection); + void Unsubscribe(CallSubscriberConnection connection); } } \ No newline at end of file diff --git a/src/PhoneBox.Server/SignalR/TelephonyHub.cs b/src/PhoneBox.Server/SignalR/TelephonyHub.cs index 9389eec..7e3a890 100644 --- a/src/PhoneBox.Server/SignalR/TelephonyHub.cs +++ b/src/PhoneBox.Server/SignalR/TelephonyHub.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using PhoneBox.Abstractions; @@ -13,15 +15,41 @@ public partial class TelephonyHub : Hub public TelephonyHub(ITelephonyConnector connector, ILogger logger) { _connector = connector; - this._logger = logger; + _logger = logger; } public override Task OnConnectedAsync() { - string userid = Context.UserIdentifier!; - _connector.Subscribe(new CallSubscriber(userid)); - _logger.LogInformation("Client connected: {Userid}", userid); + string connectionId = Context.ConnectionId; + string userid = GetUserIdSafe(); + _connector.Subscribe(new CallSubscriberConnection(connectionId, new CallSubscriber(userid))); + _logger.LogInformation("Client connected: [ConnectionId: {ConnectionId}] [UserId: {Userid}]", connectionId, userid); return Task.CompletedTask; } + + public override Task OnDisconnectedAsync(Exception? exception) + { + string connectionId = Context.ConnectionId; + string userid = GetUserIdSafe(); + _connector.Unsubscribe(new CallSubscriberConnection(connectionId, new CallSubscriber(userid))); + StringBuilder sb = new StringBuilder("Client disconnected: [ConnectionId: {ConnectionId}] [UserId: {Userid}]"); + if (exception != null) + { + sb.Append(@" +Exception: {Exception}"); + } + string message = sb.ToString(); + _logger.LogInformation(message, connectionId, userid, exception); + return Task.CompletedTask; + } + + private string GetUserIdSafe() + { + string? userIdentifier = Context.UserIdentifier; + if (String.IsNullOrEmpty(userIdentifier)) + throw new InvalidOperationException("Unresolved user identifier from SignalR connection"); + + return userIdentifier; + } } } \ No newline at end of file diff --git a/src/PhoneBox.Server/WebHook/WebHookConnector.cs b/src/PhoneBox.Server/WebHook/WebHookConnector.cs index 0a2dc9a..e9ab450 100644 --- a/src/PhoneBox.Server/WebHook/WebHookConnector.cs +++ b/src/PhoneBox.Server/WebHook/WebHookConnector.cs @@ -4,6 +4,7 @@ namespace PhoneBox.Server.WebHook { internal sealed class WebHookConnector : ITelephonyConnector { - void ITelephonyConnector.Subscribe(CallSubscriber subscriber) { } + void ITelephonyConnector.Subscribe(CallSubscriberConnection connection) { } + void ITelephonyConnector.Unsubscribe(CallSubscriberConnection connection) { } } } \ No newline at end of file diff --git a/src/PhoneBox.TapiService/TapiConnector.cs b/src/PhoneBox.TapiService/TapiConnector.cs index 007f0f5..af3d3b3 100644 --- a/src/PhoneBox.TapiService/TapiConnector.cs +++ b/src/PhoneBox.TapiService/TapiConnector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -13,7 +14,7 @@ public sealed class TapiConnector : IHostedService, ITelephonyConnector { private readonly ITelephonyEventDispatcherFactory _eventDispatcherFactory; private readonly ILogger _logger; - private readonly TapiEventNotificationSink _callNotification; + private readonly TapiEventNotificationSink _callNotificationSink; private TAPIClass? _tapiClient; @@ -21,14 +22,14 @@ public TapiConnector(ITelephonyEventDispatcherFactory eventDispatcherFactory, IL { _eventDispatcherFactory = eventDispatcherFactory; _logger = logger; - _callNotification = new TapiEventNotificationSink(); + _callNotificationSink = new TapiEventNotificationSink(); } Task IHostedService.StartAsync(CancellationToken cancellationToken) { _tapiClient = new TAPIClass(); _tapiClient.Initialize(); - _tapiClient.ITTAPIEventNotification_Event_Event += _callNotification.Event; + _tapiClient.ITTAPIEventNotification_Event_Event += _callNotificationSink.Event; _tapiClient.EventFilter = (int)( TAPI_EVENT.TE_CALLNOTIFICATION | TAPI_EVENT.TE_CALLSTATE | @@ -43,31 +44,41 @@ Task IHostedService.StartAsync(CancellationToken cancellationToken) Task IHostedService.StopAsync(CancellationToken cancellationToken) { - _tapiClient!.ITTAPIEventNotification_Event_Event -= _callNotification!.Event; - _tapiClient!.Shutdown(); + TAPIClass client = GetClientSafe(); + client.ITTAPIEventNotification_Event_Event -= _callNotificationSink!.Event; + client.Shutdown(); return Task.CompletedTask; } - void ITelephonyConnector.Subscribe(CallSubscriber subscriber) + void ITelephonyConnector.Subscribe(CallSubscriberConnection connection) { + CallSubscriber subscriber = connection.Subscriber; ITAddress? tapiAddress = FindTapiAddressForSubscriber(subscriber); if (tapiAddress == null) { - // Boom? + throw new InvalidOperationException($"Could not resolve TAPI address by phone number: {subscriber.PhoneNumber}"); + } + + if (_callNotificationSink.IsRegistered(tapiAddress)) + { + _callNotificationSink.AddConnectionToSubscription(connection); } else { - _tapiClient!.RegisterCallNotifications(tapiAddress, fMonitor: true, fOwner: true, lMediaTypes: TapiConstants.TAPIMEDIATYPE_AUDIO, lCallbackInstance: 0); - + int subscriptionId = GetClientSafe().RegisterCallNotifications(tapiAddress, fMonitor: true, fOwner: true, lMediaTypes: TapiConstants.TAPIMEDIATYPE_AUDIO, lCallbackInstance: 0); ITelephonyEventDispatcher eventDispatcher = _eventDispatcherFactory.Create(subscriber); - _callNotification.AddAddressRegistration(tapiAddress, subscriber, eventDispatcher); + _callNotificationSink.AddSubscriber(tapiAddress, connection, eventDispatcher, subscriptionId); } + } + void ITelephonyConnector.Unsubscribe(CallSubscriberConnection connection) + { + _callNotificationSink.RemoveSubscriber(connection, GetClientSafe()); } private ITAddress? FindTapiAddressForSubscriber(CallSubscriber subscriber) { - IEnumAddress addresses = _tapiClient!.EnumerateAddresses(); + IEnumAddress addresses = GetClientSafe().EnumerateAddresses(); uint fetched = 0; do { @@ -91,19 +102,62 @@ void ITelephonyConnector.Subscribe(CallSubscriber subscriber) return null; } + private TAPIClass GetClientSafe() + { + if (_tapiClient == null) + throw new InvalidOperationException("Tapi client not initialized"); + + return _tapiClient; + } + private sealed class TapiEventNotificationSink : ITTAPIEventNotification { - private readonly IDictionary _registrations; + private readonly IDictionary _addressSubscriptionMap; + private readonly IDictionary _subscriberAddressMap; public TapiEventNotificationSink() { - _registrations = new SortedDictionary(); + _addressSubscriptionMap = new SortedDictionary(); + _subscriberAddressMap = new Dictionary(); } - public void AddAddressRegistration(ITAddress address, CallSubscriber subscriber, ITelephonyEventDispatcher eventDispatcher) + public bool IsRegistered(ITAddress tapiAddress) => _addressSubscriptionMap.ContainsKey(tapiAddress.AddressName); + + public void AddConnectionToSubscription(CallSubscriberConnection connection) { - var addressRegistration = new TapiAddressSubscription(address, subscriber, eventDispatcher); - _registrations.Add(address.AddressName, addressRegistration); + TapiAddressSubscription subscription = GetSubscription(connection.Subscriber); + subscription.Connections.Add(connection); + } + + public void AddSubscriber(ITAddress address, CallSubscriberConnection connection, ITelephonyEventDispatcher eventDispatcher, int subscriptionId) + { + string addressName = address.AddressName; + if (_addressSubscriptionMap.ContainsKey(addressName)) + { + throw new InvalidOperationException(@$"Address already registered: {addressName} +ConnectionId: {connection.ConnectionId} +SubscriberPhoneNumber: {connection.Subscriber.PhoneNumber}"); + } + + var addressRegistration = new TapiAddressSubscription(address, eventDispatcher, subscriptionId, connection); + _addressSubscriptionMap.Add(addressName, addressRegistration); + _subscriberAddressMap.Add(connection.Subscriber.PhoneNumber, addressRegistration); + } + + public void RemoveSubscriber(CallSubscriberConnection connection, ITTAPI client) + { + CallSubscriber subscriber = connection.Subscriber; + TapiAddressSubscription subscription = GetSubscription(subscriber); + if (subscription.Connections.Count < 2) + { + client.UnregisterNotifications(subscription.SubscriptionId); + _subscriberAddressMap.Remove(subscriber.PhoneNumber); + _addressSubscriptionMap.Remove(subscription.AddressName); + } + else + { + subscription.Connections.Remove(connection); + } } public async void Event(TAPI_EVENT tapiEvent, object pEvent) @@ -119,7 +173,7 @@ public async void Event(TAPI_EVENT tapiEvent, object pEvent) private async Task PublishCallStateEvent(ITCallStateEvent stateEvent) { ITCallInfo call = stateEvent.Call; - if (!_registrations.TryGetValue(call.Address.AddressName, out TapiAddressSubscription registration)) + if (!_addressSubscriptionMap.TryGetValue(call.Address.AddressName, out TapiAddressSubscription registration)) return; string phoneNumber = call.CallInfoString[CALLINFO_STRING.CIS_CALLERIDNUMBER]; @@ -134,19 +188,29 @@ private async Task PublishCallStateEvent(ITCallStateEvent stateEvent) break; } } + + private TapiAddressSubscription GetSubscription(CallSubscriber subscriber) + { + if (!_subscriberAddressMap.TryGetValue(subscriber.PhoneNumber, out TapiAddressSubscription subscription)) + throw new InvalidOperationException($"No subscriptions registered for phone number: {subscriber.PhoneNumber}"); + + return subscription; + } } private readonly struct TapiAddressSubscription { public string AddressName { get; } - public CallSubscriber Subscriber { get; } public ITelephonyEventDispatcher Publisher { get; } + public int SubscriptionId { get; } + public ICollection Connections { get; } - public TapiAddressSubscription(ITAddress address, CallSubscriber subscriber, ITelephonyEventDispatcher publisher) + public TapiAddressSubscription(ITAddress address, ITelephonyEventDispatcher publisher, int subscriptionId, CallSubscriberConnection connection) { AddressName = address.AddressName; - Subscriber = subscriber; Publisher = publisher; + SubscriptionId = subscriptionId; + Connections = new Collection { connection }; } } }