commit d8621466425a591e6c61f0eb01a07920e6c2cc06 Author: Hadi Khazaee Asl Date: Thu Jan 25 04:47:14 2024 +0330 Initial Commit ... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bd9d0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# +# DotNet ... +bin +obj + +# +# Natural Docs ... +Documentation/* diff --git a/Base/XBaseHub.cs b/Base/XBaseHub.cs new file mode 100644 index 0000000..b6b23f1 --- /dev/null +++ b/Base/XBaseHub.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using xCommons.Extensions; +using xModels.Dtos; +using xPushService.Constants; +using xPushService.Interfaces; +using xPushService.Models; + +namespace xPushService.Base { + /// + /// a base class for implementing Hubs ... + /// + public abstract class XBaseHub : Hub { + // + #region Props ... + protected readonly IXPushGroupStore groupStore; + protected readonly IXPushConnectionStore connectionStore; + #endregion + + // + #region Constructor ... + public XBaseHub ( + IXPushGroupStore groupStore, + IXPushConnectionStore connectionStore + ) : base () { + // + this.groupStore = groupStore; + this.connectionStore = connectionStore; + } + #endregion + + // + #region Abstract ... + #endregion + + // + #region EnevtHandlers ... + #endregion + + // + #region Overrides ... + public override Task OnConnectedAsync () { + // + var connection = GetConnection (); + if (!connection.IsNull ()) { + // + connectionStore + .Add (connection) + .RunTask (); + + // + var count = connectionStore + .Count () + .RunTask (); + + // + Console.WriteLine ($"Count After Coonected: {count}"); + } + + // + return base.OnConnectedAsync (); + } + + public override Task OnDisconnectedAsync (Exception exception) { + // + var connection = GetConnection (); + if (!connection.IsNull ()) { + // + connectionStore + .RemoveByConnectionId (connection.Id) + .RunTask (); + + // + var count = connectionStore + .Count () + .RunTask (); + + // + Console.WriteLine ($"Count After DisCoonected: {count}"); + } + + // + return base.OnDisconnectedAsync (exception); + } + #endregion + + // + #region Actions ... + /// + /// Notify to all Other Clients which a new Connection is established ... + /// + /// + public Task NotifyNewConnection (string connectionId) { + // + var actor = Context.User.Identity.Name; + var authType = Context.User.Identity.AuthenticationType; + var actorRole = Context?.User?.FindFirst ("role")?.Value; + + // + var pushMessage = new XPushMessage { + // + Actor = actor, + Payload = connectionId, + Type = XPushType.System, + TimeStamp = DateTime.UtcNow, + Topic = (int) XPushType.System, + Action = XBasePushAction.NotifyNewConnection.GetStringValue (), + Message = $"a new connection stablished by {connectionId} ...", + }; + + // + Console.WriteLine ($"XPushServiceLog => "); + Console.WriteLine ($"XPushServiceLog => NotifyNewConnection: User: {actor}/Role: {actorRole}/AuthType: {authType}/ConnectionId: {connectionId}/ConnectedAt: {DateTime.UtcNow}"); + Console.WriteLine ($"XPushServiceLog => "); + + // + return Clients.Others.SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + pushMessage + ); + } + + /// + /// Update Last Seen ... + /// + public Task UpdateLastSeen () { + var connectionId = Context.ConnectionId; + return connectionStore + .UpdateLastSeen (connectionId) + .ContinueWith (updateLastSeenTask => { + // + var updateLastSeen = updateLastSeenTask + .RunTask (); + if (updateLastSeen) { + // + var updatedConnection = connectionStore + .GetByConnectionId (connectionId) + .RunTask (); + + // + var count = connectionStore + .Count () + .RunTask (); + + // + Console.WriteLine ($"Connection Last Seen Updated: {connectionId}: {updatedConnection.LastSeen}, Connection Count: {count} ..."); + } + }); + } + + /// + /// publicly Push a Message to Other Clients ... + /// + /// + /// + public Task PushMessageAsync (XPushMessage message) { + // + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients.Others.SendAsync (XBasePushAction.PushMessage.GetStringValue (), message); + } + + /// + /// publicly Push a Message to Other Clients ... + /// + /// + /// + [Authorize] + public Task AuthorizedMessageAsync (XPushMessage message) { + // + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients.Others.SendAsync (XBasePushAction.PushMessage.GetStringValue (), message); + } + + /// + /// push message to All connections ... + /// + /// + /// + public Task PushMessageToAll (XPushMessage message) { + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients + .All + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ); + } + + /// + /// push message to specific connection ... + /// + /// + /// + /// + public Task PushMessageToConnection ( + string connectionId, + XPushMessage message + ) { + // + // Validate Args ... + if (connectionId.IsNullOrEmpty ()) { + return Task.CompletedTask; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients + .Client (connectionId) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ); + } + + /// + /// push message to specific connections ... + /// + /// + /// + /// + public Task PushMessageToConnections ( + IEnumerable connectionIds, + XPushMessage message + ) { + // + // Validate Args ... + if (!connectionIds.HasChild ()) { + return Task.CompletedTask; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients + .Clients ( + connectionIds + .ToList () + .AsReadOnly () + ) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ); + } + #endregion + + // + #region Protected ... + /// + /// retrieve connected user name ... + /// + /// + protected string GetUserName () { + return Context?.User?.Identity?.Name; + } + + /// + /// retrieve connected user role ... + /// + /// + protected string GetUserRole () { + return Context?.User?.FindFirst ("role")?.Value; + } + + /// + /// Retrieve Empty Message ... + /// + /// + protected XPushMessage GetPushMessage () { + // + var timeStamp = DateTime.UtcNow; + var actor = Context.User.Identity.Name; + var connectionId = Context.ConnectionId; + + // + var result = new XPushMessage { + Actor = actor, + TimeStamp = timeStamp, + Payload = connectionId, + }; + + // + return result; + } + + /// + /// retrieve current connection id ... + /// + /// + protected string GetConnectionId () { + return Context.ConnectionId; + } + + /// + /// Retrieve Connection Model from Context ... + /// + /// + protected XPushConnectionDto GetConnection () { + // + var connectionId = Context.ConnectionId; + var userName = Context.User.Identity.Name; + var authenticationType = Context.User.Identity.AuthenticationType; + + // + var httpContext = Context.GetHttpContext (); + var deviceJson = httpContext.Request.Query["Option"].ToString (); + var device = deviceJson.FromJSON (); + + // + var result = new XPushConnectionDto { + User = userName, + Device = device, + Id = connectionId, + LastSeen = DateTime.UtcNow + }; + + // + return result; + } + #endregion + + // + #region Private ... + #endregion + } +} \ No newline at end of file diff --git a/Base/XBaseHubClient.cs b/Base/XBaseHubClient.cs new file mode 100644 index 0000000..6e999ae --- /dev/null +++ b/Base/XBaseHubClient.cs @@ -0,0 +1,87 @@ +using System; +using System.Reactive.Subjects; +using Microsoft.AspNetCore.SignalR.Client; +using xPushService.Configurations; +using xPushService.Models; +using xPushService.Policies; + +namespace xPushService.Base { + // TODO: Complete this ... + public class XBaseHubClient { + // + #region Props ... + // + // private string CONNECTION_ID; + // private HubConnection HUB_CONNECTION; + // private IDisposable INTERVAL_SUBSCRIPTION; + // private XPushServiceConnectionRetryPolicy HUB_CONNECTION_POLICY; + + // + public Subject MessageReceived = new Subject (); + public Subject UserMessageReceived = new Subject (); + public Subject SystemMessageReceived = new Subject (); + + // + public Subject OnClose = new Subject (); + public Subject OnReconnected = new Subject (); + public Subject OnReconnecting = new Subject (); + + /// + /// readonly connection id ... + /// + /// + // public string ConnectionId { + // get { + // return this.CONNECTION_ID; + // } + // } + + /// + /// readonly connection object ... + /// + /// + // public HubConnection Connection { + // get { + // return this.HUB_CONNECTION; + // } + // } + #endregion + + // + #region Constructor ... + public XBaseHubClient ( + XPushServiceConfiguration config + ) { + + } + #endregion + + // + #region Abstract ... + #endregion + + // + #region Protected ... + #endregion + + // + #region Actions ... + #endregion + + // + #region IXAuthorizedPushActions ... + #endregion + + // + #region IXUserPushActions ... + #endregion + + // + #region IXGroupsPushActions ... + #endregion + + // + #region Private ... + #endregion + } +} \ No newline at end of file diff --git a/Base/XBaseWebRTCHub.cs b/Base/XBaseWebRTCHub.cs new file mode 100644 index 0000000..7da98c9 --- /dev/null +++ b/Base/XBaseWebRTCHub.cs @@ -0,0 +1,836 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using xCommons.Extensions; +using xExceptions.Constants; +using xModels.Dtos; +using xPushService.Constants; +using xPushService.Extensions; +using xPushService.Interfaces; +using xPushService.Models; + +namespace xPushService.Base { + public abstract class XBaseWebRTCHub : XBaseHub { + // + #region Props ... + protected readonly IXWebRTCConnectionStore webRTCConnectionStore; + #endregion + + // + #region Constructor ... + public XBaseWebRTCHub ( + IXPushGroupStore groupStore, + IXPushConnectionStore connectionStore, + IXWebRTCConnectionStore webRTCConnectionStore + ) : base (groupStore, connectionStore) { + this.webRTCConnectionStore = webRTCConnectionStore; + } + #endregion + + // + #region Overrides ... + public override Task OnConnectedAsync () { + return base.OnConnectedAsync (); + } + + public override Task OnDisconnectedAsync (Exception exception) { + // // + // var connectionId = GetConnectionId (); + + // // + // // Find Connections Active Calls ... + // var activeConnections = webRTCConnectionStore + // .FindMany (c => c.CallerConnectionId + // .ToNormalString () == connectionId + // .ToNormalString () || + // c.IsInCallees (connectionId) + // ) + // .RunTask (); + + // // + // if (activeConnections.HasChild ()) { + // activeConnections + // .ToList () + // .ForEach (c => { + // // + // CancelCall ( + // cancellerConnectionId: connectionId, + // reason: XCallEndReason.Disconnected, + // request: c + // ) + // .RunTask (); + // }); + // } + + // + return base.OnDisconnectedAsync (exception); + } + #endregion + + // + #region Actions ... + /// + /// Send PreOffer Call Request ... + /// + /// + /// + /// + /// + public Task RequestCall ( + XCallType type, + string connectionId, + XDeviceDto device + ) { + // + // Validate Args ... + if (type == XCallType.None || connectionId.IsNullOrEmpty ()) { + return Task.FromResult (false); + } + + // + // Check Connection Exists ... + var isConnectionExists = connectionStore + .IsExistsConnectionId (connectionId) + .RunTask (); + if (!isConnectionExists) { + return Task.FromResult (false); + } + + // + // retrieve connection object ... + var request = GetWebRTCConnection ( + calleeDevice: device, + connectionType: type, + calleeConnectionId: connectionId + ) + .RunTask (); + + // + // Add Connection to Store ... + AddOrUpdateWebRTCConnection (request) + .RunTask (); + + // + try { + // + Clients + .Clients ( + request + .GetReceivers () + ) + .SendAsync ( + XWebRTCAction.RequestCall.GetStringValue (), + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { } + + // + return Task.FromResult (false); + } + + /// + /// Send PreOffer Call Request ... + /// + /// + /// + /// + /// + public Task RequestUserCall ( + XCallType type, + string user, + XDeviceDto device + ) { + // + // Validate Args ... + if (type == XCallType.None || user.IsNullOrEmpty ()) { + return Task.FromResult (false); + } + + // + // Check Connection Exists ... + var isConnectionExists = connectionStore + .IsExistsUser (user) + .RunTask (); + if (!isConnectionExists) { + return Task.FromResult (false); + } + + // + // retrieve connection object ... + var request = GetUserWebRTCConnection ( + calleeDevice: device, + connectionType: type, + calleeUser: user + ) + .RunTask (); + + // + // Add Connection to Store ... + AddOrUpdateWebRTCConnection (request) + .RunTask (); + + // + try { + // + Clients + .Clients ( + request + .GetReceivers () + ) + .SendAsync ( + XWebRTCAction.RequestCall.GetStringValue (), + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { } + + // + return Task.FromResult (false); + } + + /// + /// Cancel or End Requested Call ... + /// + /// + public Task CancelCall ( + string cancellerConnectionId, + XCallEndReason reason, + XWebRTCConnectionDto request) { + // + // Validate Args ... + var isValid = IsWebRTCConnectionsExists (request) + .RunTask (); + if (!isValid) { + return Task.FromResult (false); + } + + // + // Check if i am Caller ... + var isCancellerCaller = request + .IsCaller (cancellerConnectionId); + var isCancellerInCallees = request + .IsInCallees (cancellerConnectionId); + + // + if (isCancellerCaller) { + // + request.Callees.ToList ().ForEach (callee => { + // + callee.EndReason = reason; + callee.EndTime = DateTime.UtcNow; + }); + + // + // Remove Connection From Store ... + webRTCConnectionStore + .Remove (request) + .RunTask (); + } else if (isCancellerInCallees) { + // + var canceller = request.GetCallee (cancellerConnectionId); + if (canceller.IsNull ()) { + return Task.FromResult (false); + } + + // + var updatedCanceller = canceller; + updatedCanceller.EndReason = reason; + updatedCanceller.EndTime = DateTime.UtcNow; + + // + request.Callees = request.Callees + .Update (canceller, updatedCanceller); + + // + // Remove connection from Store if all callees end calls ... + var canRemoveConnection = request.Callees.All (c => !c.EndTime.IsNull () && !c.EndReason.IsNull ()); + if (canRemoveConnection) { + webRTCConnectionStore + .Remove (request) + .RunTask (); + } else { + webRTCConnectionStore + .Update (request) + .RunTask (); + } + } + + // + try { + // + Clients + .Clients ( + request.GetReceivers () + ) + .SendAsync ( + XWebRTCAction.CancelCall.GetStringValue (), + request + ); + + // + return Task.FromResult (true); + } catch { + return Task.FromResult (false); + } + } + + /// + /// end a call request ... + /// + /// + /// + public Task RejectCall ( + string rejecterConnectionId, + XWebRTCConnectionDto request + ) { + // + // Validate Args ... + var isValid = IsWebRTCConnectionsExists (request) + .RunTask (); + var isRejecterInCallees = request + .IsInCallees (rejecterConnectionId); + if (!isValid || + !isRejecterInCallees || + rejecterConnectionId.IsNullOrEmpty () + ) { + return Task.FromResult (false); + } + + // + // Find Rejecter and Push Reject to it ... + var rejecter = request + .GetCallee (rejecterConnectionId); + if (rejecter.IsNull ()) { + return Task.FromResult (false); + } + + // + // Update request Callees ... + var updatedRejecter = rejecter; + updatedRejecter.EndTime = DateTime.UtcNow; + updatedRejecter.EndReason = XCallEndReason.Reject; + request.Callees = request.Callees + .Update (rejecter, updatedRejecter); + + // + // Check if Only one Callee and he Reject, Remove connection From Store, + // otherwise Update connection ... + var canRemoveConnection = request + .Callees.Count () == 1 && request.Callees + .All (c => c.ConnectionId + .ToNormalString () == rejecterConnectionId + .ToNormalString () + ); + if (canRemoveConnection) { + webRTCConnectionStore + .Remove (request) + .RunTask (); + } + + // + // Prepare Receivers ... + var receivers = new List () { + request.CallerConnectionId, + rejecterConnectionId + } + .AsReadOnly (); + + // + try { + // + Clients + .Clients (receivers) + .SendAsync ( + XWebRTCAction.RejectCall.GetStringValue (), + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { } + + // + return Task.FromResult (false); + } + + /// + /// answer a call request ... + /// + /// + /// + public Task AcceptCall ( + string accepterConnectionId, + XWebRTCConnectionDto request + ) { + // + // Validate Args ... + var isValid = IsWebRTCConnectionsExists (request) + .RunTask (); + var isAccepterInCallees = request + .IsInCallees (accepterConnectionId); + if (!isValid || + !isAccepterInCallees || + accepterConnectionId.IsNullOrEmpty () + ) { + return Task.FromResult (false); + } + + // + // Add Or Update ... + AddOrUpdateWebRTCConnection (request) + .RunTask (); + + // + // prepare recievers ... + var receivers = new List () { + request.CallerConnectionId, + accepterConnectionId, + } + .AsReadOnly (); + + // + try { + // + Clients + .Clients (receivers) + .SendAsync ( + XWebRTCAction.AcceptCall.GetStringValue (), + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { + return Task.FromResult (false); + } + } + #endregion + + // + #region WebRTC Signaling ... + /// + /// Send WebRTC Offer ... + /// + /// + /// + /// + public Task WebRTCOffer ( + XWebRTCSignalDto signal, + XWebRTCConnectionDto request + ) { + // + if ( + signal.IsNull () || + signal.Offer.IsNull () || + !IsValidWebRTCConnection (request) + ) { + return Task.FromResult (false); + } + + // + var connectionId = GetConnectionId (); + var offerRecievers = request + .GetReceivers () + .Where (r => r.ToNormalString () != connectionId.ToNormalString ()) + .ToList () + .AsReadOnly (); + + // + try { + // + Clients + .Clients (offerRecievers) + .SendAsync ( + XWebRTCAction.WebRTCOffer.GetStringValue (), + signal, + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { + return Task.FromResult (false); + } + } + + /// + /// Send WebRTC Answer ... + /// + /// + /// + /// + public Task WebRTCAnswer ( + XWebRTCSignalDto signal, + XWebRTCConnectionDto request + ) { + // + if ( + signal.IsNull () || + signal.Offer.IsNull () || + signal.Answer.IsNull () || + !IsValidWebRTCConnection (request) + ) { + return Task.FromResult (false); + } + + // + var connectionId = GetConnectionId (); + var answerRecievers = request + .GetReceivers () + .Where (r => r.ToNormalString () != connectionId.ToNormalString ()) + .ToList () + .AsReadOnly (); + + // + try { + // + Clients + .Clients (answerRecievers) + .SendAsync ( + XWebRTCAction.WebRTCAnswer.GetStringValue (), + signal, + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { + return Task.FromResult (false); + } + } + + /// + /// Send WebRTC ICE Candidates ... + /// + /// + /// + /// + public Task WebRTCICECandidate ( + XWebRTCSignalDto signal, + XWebRTCConnectionDto request + ) { + // + if ( + signal.IsNull () || + signal.Offer.IsNull () || + signal.Answer.IsNull () || + !signal.Cadidates.HasChild () || + !IsValidWebRTCConnection (request) + ) { + return Task.FromResult (false); + } + + // + var connectionId = GetConnectionId (); + var candidatesRecievers = request + .GetReceivers () + .Where (r => r.ToNormalString () != connectionId.ToNormalString ()) + .ToList () + .AsReadOnly (); + + // + try { + // + Clients + .Clients (candidatesRecievers) + .SendAsync ( + XWebRTCAction.WebRTCICECandidate.GetStringValue (), + signal, + request + ) + .RunTask (); + + // + return Task.FromResult (true); + } catch { + return Task.FromResult (false); + } + } + #endregion + + // + #region Protected ... + /// + /// create first time WebRTC connection by user name ... + /// + /// + /// + /// + /// + protected Task GetUserWebRTCConnection ( + XCallType connectionType, + string calleeUser, + XDeviceDto calleeDevice + ) { + // + // Validate Args ... + if ( + calleeDevice.IsNull () || + connectionType == XCallType.None || + calleeUser.IsNullOrEmpty () + ) { + throw XException.InvalidArgs.ToException (); + } + + // + var id = Guid.NewGuid ().ToString (); + + // + var httpContext = Context.GetHttpContext (); + var deviceJson = httpContext.Request.Query["Option"].ToString (); + + // + // Retrieve Caller Info ... + var callerConnectionId = Context.ConnectionId; + var callerUserName = Context?.User?.Identity?.Name; + var callerDevice = deviceJson.FromJSON (); + + // + // Retrieve Connection ... + var calleeConnection = connectionStore + .FindOne (c => c.User.ToNormalString () == calleeUser.ToNormalString ()) + .RunTask (); + + // + var result = new XWebRTCConnectionDto { + // + Id = id, + // + Payload = null, + Type = connectionType, + // + RequestTime = DateTime.UtcNow, + // + CallerUser = callerUserName, + CallerDevice = callerDevice, + CallerConnectionId = callerConnectionId, + // + Callees = new List { + new XWebRTCCalleeDto { + Device = calleeDevice, + User = calleeConnection.User, + ConnectionId = calleeConnection.Id, + } + } + }; + + // + return Task.FromResult (result); + } + + /// + /// create first time WebRTC connection ... + /// + /// + /// + /// + /// + protected Task GetWebRTCConnection ( + XCallType connectionType, + string calleeConnectionId, + XDeviceDto calleeDevice + ) { + // + // Validate Args ... + if ( + calleeDevice.IsNull () || + connectionType == XCallType.None || + calleeConnectionId.IsNullOrEmpty () + ) { + throw XException.InvalidArgs.ToException (); + } + + // + var id = Guid.NewGuid ().ToString (); + + // + var httpContext = Context.GetHttpContext (); + var deviceJson = httpContext.Request.Query["Option"].ToString (); + + // + // Retrieve Caller Info ... + var callerConnectionId = Context.ConnectionId; + var callerUserName = Context?.User?.Identity?.Name; + var callerDevice = deviceJson.FromJSON (); + + // + // Retrieve Connection ... + var calleeConnection = connectionStore + .GetByConnectionId (calleeConnectionId) + .RunTask (); + + // + var result = new XWebRTCConnectionDto { + // + Id = id, + // + Payload = null, + Type = connectionType, + // + RequestTime = DateTime.UtcNow, + // + CallerUser = callerUserName, + CallerDevice = callerDevice, + CallerConnectionId = callerConnectionId, + // + Callees = new List { + new XWebRTCCalleeDto { + Device = calleeDevice, + User = calleeConnection.User, + ConnectionId = calleeConnectionId, + } + } + }; + + // + return Task.FromResult (result); + } + + /// + /// Check i am caller or not ... + /// + /// + /// + protected bool AmICaller (XWebRTCConnectionDto request) { + // + // Validate Args ... + if (!request.IsValid () || + !IsValidWebRTCConnection (request) + ) { + throw XException.InvalidArgs.ToException (); + } + + // + var result = request.IsCaller (GetConnectionId ()); + return result; + } + + /// + /// Check i am callee or not ... + /// + /// + /// + protected bool AmICallee (XWebRTCConnectionDto request) { + // + // Validate Args ... + if (!request.IsValid () || + !IsValidWebRTCConnection (request)) { + throw XException.InvalidArgs.ToException (); + } + + // + var result = request.IsInCallees (GetConnectionId ()); + return result; + } + + /// + /// validate a web rtc connection ... + /// + /// + /// + protected bool IsValidWebRTCConnection (XWebRTCConnectionDto request) { + // + // Validate Args ... + if ( + request.IsNull () || + !request.IsValid () || + !request.Callees.HasChild () || + request.Type == XCallType.None || + request.CallerConnectionId.IsNullOrEmpty () + ) { + return false; + } + + // + return true; + } + + /// + /// check bot Caller and Callee Connections Exists ... + /// + /// + /// + protected Task IsWebRTCConnectionsExists (XWebRTCConnectionDto request) { + // + // Validate Args ... + if (!IsValidWebRTCConnection (request)) { + return Task.FromResult (false); + } + + // + var isCallerConnectionExists = connectionStore + .IsExistsConnectionId (request.CallerConnectionId) + .RunTask (); + + // + var isCalleesConnectionExists = !request.Callees.HasChild () ? true : + Task.WhenAll (request.Callees.Select (callee => connectionStore + .IsExistsConnectionId (callee.ConnectionId))) + .ContinueWith (allTasks => { + // + var allResults = allTasks + .RunTask (); + + // + return allResults.All (r => r == true); + }) + .RunTask (); + + // + if (!isCallerConnectionExists || !isCalleesConnectionExists) { + return Task.FromResult (false); + } + + // + return Task.FromResult (true); + } + + /// + /// Add or Update a WebRTCConnection To/In Store ... + /// + /// + /// + protected Task AddOrUpdateWebRTCConnection (XWebRTCConnectionDto request) { + // + if (!request.IsValid () || + request.Id.IsNullOrEmpty () + ) { + return Task.FromResult (false); + } + + // + // Check Exists or not ... + var isWebRTCConnectionExists = webRTCConnectionStore + .IsExistsByKey (request.Id) + .RunTask (); + + // + // Act based on existance ... + if (!isWebRTCConnectionExists) { + return webRTCConnectionStore.Add (request); + } else { + // + var updated = webRTCConnectionStore.Update (request) + .RunTask (); + var isUpdate = !updated.IsNull (); + + // + return Task.FromResult (isUpdate); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Configurations/XPushServiceConfiguration.cs b/Configurations/XPushServiceConfiguration.cs new file mode 100644 index 0000000..c30c0e0 --- /dev/null +++ b/Configurations/XPushServiceConfiguration.cs @@ -0,0 +1,46 @@ +namespace xPushService.Configurations { + public partial class XPushServiceConfiguration { + /// + /// Base route of WebSocket Server ... + /// + /// string + public string BaseRoute { get; set; } + + /// + /// main action of push messages ... + /// + /// + public string PushMessageAction { get; set; } + + /// + /// determines log level of signalR ... + /// + /// + public object ConnectionLogLevel { get; set; } + + /// + /// when a client connected it's going to update last seen of a client by this interval ... + /// + /// + public int UpdateLastSeenInterval { get; set; } + + /// + /// when client disconnected it's automatically try to reconnect, this + /// determines max number of try to connect ... + /// + /// + public int PushConnectionMaxRetry { get; set; } + + /// + /// add support for message protocol ... + /// + /// + public bool AddSupportMessageProtocol { get; set; } + + /// + /// the delay between two connection try .. + /// + /// + public int PushConnectionReconnectDelay { get; set; } + } +} \ No newline at end of file diff --git a/Constants/XCallEndReason.cs b/Constants/XCallEndReason.cs new file mode 100644 index 0000000..9fedb52 --- /dev/null +++ b/Constants/XCallEndReason.cs @@ -0,0 +1,9 @@ +namespace xPushService.Constants { + public enum XCallEndReason { + End, + Missed, + Reject, + Canceled, + Disconnected + } +} \ No newline at end of file diff --git a/Constants/XCallType.cs b/Constants/XCallType.cs new file mode 100644 index 0000000..b642a49 --- /dev/null +++ b/Constants/XCallType.cs @@ -0,0 +1,11 @@ +namespace xPushService.Constants { + public enum XCallType { + None, + Chat, + AudioCall, + VideoCall, + DesktopShare, + AudioConference, + VideoConference, + } +} \ No newline at end of file diff --git a/Constants/XPushServiceConstants.cs b/Constants/XPushServiceConstants.cs new file mode 100644 index 0000000..748b481 --- /dev/null +++ b/Constants/XPushServiceConstants.cs @@ -0,0 +1,7 @@ +namespace xPushService.Constants { + public partial class XPushServiceConstants { + public partial struct XConfigurationNodes { + public const string XPushServiceConfiguration = "PushServiceConfiguration"; + } + } +} \ No newline at end of file diff --git a/Constants/XPushType.cs b/Constants/XPushType.cs new file mode 100644 index 0000000..59394a0 --- /dev/null +++ b/Constants/XPushType.cs @@ -0,0 +1,30 @@ +using xExceptions.Attributes; + +namespace xPushService.Constants { + /// + /// Determines Push Notification Types + /// + public enum XPushType { + /// System Type Notifications Relate Only to System + System, + + /// User Specific Types Of Push Notifications + User + } + + /// + /// Base Push Actions ... + /// + public enum XBasePushAction { + [StringValue ("PushMessageAsync")] + PushMessage, + [StringValue ("UserMessageAsync")] + UserMessage, + [StringValue ("AdminMessageAsync")] + AdminMessage, + [StringValue("AuthorizedMessageAsync")] + AuthorizedMessage, + [StringValue("NotifyNewConnection")] + NotifyNewConnection + } +} \ No newline at end of file diff --git a/Constants/XWebRTCAction.cs b/Constants/XWebRTCAction.cs new file mode 100644 index 0000000..ad0354d --- /dev/null +++ b/Constants/XWebRTCAction.cs @@ -0,0 +1,32 @@ +using xExceptions.Attributes; + +namespace xPushService.Constants { + public enum XWebRTCAction { + // + [StringValue ("RequestCall")] + RequestCall, + + [StringValue ("RequestUserCall")] + RequestUserCall, + + // + [StringValue ("CancelCall")] + CancelCall, + + [StringValue ("RejectCall")] + RejectCall, + + [StringValue ("AcceptCall")] + AcceptCall, + + // + [StringValue ("WebRTCOffer")] + WebRTCOffer, + + [StringValue ("WebRTCAnswer")] + WebRTCAnswer, + + [StringValue ("WebRTCICECandidate")] + WebRTCICECandidate, + } +} \ No newline at end of file diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs new file mode 100644 index 0000000..f3418fc --- /dev/null +++ b/DI/XDIHelperExtension.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using xCommons.Extensions; +using xExceptions.Constants; +using xPushService.Configurations; +using xPushService.Constants; +using xPushService.Helpers; +using xPushService.Interfaces; +using xPushService.Providers; +using xPushService.Store; + +namespace xPushService.DI { + public static partial class XDIHelperExtension { + /// + /// Extract Module Configuration from provided IConfiguration + /// + /// + /// an instance of XPushServiceConfiguration + public static XPushServiceConfiguration GetXPushServiceConfiguration (this IConfiguration config) { + // + var xPushConfigSection = config.GetSection (XPushServiceConstants.XConfigurationNodes.XPushServiceConfiguration); + return xPushConfigSection.Get (); + } + + /// + /// Register XPushServiceHelper ... + /// + /// + /// + public static void AddXPushServiceHelper ( + this IServiceCollection services, + XPushServiceHelper helper + ) { + // + // Validate Args ... + if (helper.IsNull ()) { + // + Log ("AddXPushServiceHelper failed, helper not provided ..."); + throw XException.InvalidArgs.ToException (); + } + + // + // Register Helper ... + services.AddSingleton (helper); + } + + /// + /// Register Module Provided Service on DI + /// + /// + /// + public static void AddXPushService ( + this IServiceCollection services, + IConfiguration config, + ServiceLifetime lifeTime = ServiceLifetime.Singleton + ) { + AddPushService ( + services, + config.GetXPushServiceConfiguration (), + lifeTime + ); + } + + /// + /// Register Module Provided Service on DI + /// + /// + /// + public static void AddXPushService ( + this IServiceCollection services, + XPushServiceConfiguration config, + ServiceLifetime lifeTime = ServiceLifetime.Singleton + ) { + AddPushService ( + services, + config, + lifeTime + ); + } + + /// + /// Use Module Middlewares on Application Builder + /// + /// + /// + public static void UseXPushService ( + this IApplicationBuilder app, + XPushServiceHelper helper + ) { + // + // Validate Args ... + var isHelperExists = !helper.IsNull (); + if (!isHelperExists) { + // + Log ("AddXPushServiceHelper failed, helper not provided ..."); + throw XException.InvalidArgs.ToException (); + } + + // + // Create an Scope for Accessing Registered Services ... + using (var scope = app.ApplicationServices.CreateScope ()) { + // + // try to Retrieve Configuration .... + var config = scope.ServiceProvider.GetService (); + if ( + config.IsNull () || + config.BaseRoute.IsNullOrEmpty () || + config.BaseRoute.Trim ().IsNullOrEmpty () + ) { + // + Log ("AddXPushServiceHelper failed, Configuration not provided ..."); + throw XException.InvalidConfiguration.ToException (); + } + + // + #region Normalize Routes ... + // + // Base Reoute ... + config.BaseRoute = config.BaseRoute.ToNormalString (); + if (!config.BaseRoute.StartsWith ("/")) { + config.BaseRoute = "/" + config.BaseRoute; + } + #endregion + + // + // add XPushService Hub to Helper ... + // + var hubRoutes = new List (); + + // + // Try to Register Hubs ... + // + // Use SignalR in App ... + app.UseSignalR (routes => { + // + helper.GetHubs ().ForEach (hubDescriptor => { + // + var hubRoute = Path.Combine (config.BaseRoute, hubDescriptor.Route); + hubRoutes.Add (hubRoute); + Console.WriteLine ($"XPushService HubRoute: {hubRoute}"); + + // + Type routesType = routes.GetType (); + MethodInfo mapHubMethod = routesType + .GetMethods () + .SingleOrDefault (m => + m.Name == "MapHub" && + m.GetParameters ().Length == 1 + ); + if (mapHubMethod.IsNull ()) { + throw XException.ActionFailed.ToException (); + } + + // + object[] genericMapHubMethodArgs = { new PathString (hubRoute) }; + MethodInfo genericMapHubMethod = mapHubMethod.MakeGenericMethod (hubDescriptor.Hub); + genericMapHubMethod.Invoke (routes, genericMapHubMethodArgs); + }); + }); + } + } + + // + #region Private ... + /// + /// a LogTag for XPushService ... + /// + private static string XLogTag = "XPushService"; + + /// + /// print a log in Console ... + /// + /// + private static void Log (string message) { + Console.WriteLine ($"{XLogTag} => {message}"); + } + + /// + /// Register Push Service ... + /// + /// + /// + /// + private static void AddPushService ( + IServiceCollection services, + XPushServiceConfiguration config, + ServiceLifetime lifeTime + ) { + // + #region Validate Args ... + // + var exception = XException.InvalidArgs.ToException ();; + + // + // Services ... + var isServicesExists = !services.IsNull (); + if (!isServicesExists) { + // + Log ("AddXPushServiceHelper failed, services not provided ..."); + throw exception; + } + + // + // Config ... + var isConfigExists = !config.IsNull (); + if (!isConfigExists) { + // + Log ("AddXPushServiceHelper failed, configuration not provided ..."); + throw exception; + } + + // + // Lifetime ... + var isLifetimeExists = !lifeTime.IsNull (); + if (!isLifetimeExists) { + // + Log ("AddXPushServiceHelper failed, lifetime not provided ..."); + throw exception; + } + #endregion + + // + // Register XPushService Configuration ... + services.AddSingleton (config); + + // + #region Try to Register Stores ... + // + var groupStore = services.GetRegisteredService (); + if (groupStore.IsNull ()) { + // + Log ($"there is no provided PushGroupStore, try to register XPushGroupInMemoryStore ..."); + groupStore = new XPushGroupInMemoryStore (); + services.AddSingleton (groupStore); + } + + // + var connectionStore = services.GetRegisteredService (); + if (connectionStore.IsNull ()) { + // + Log ($"there is no provided XPushConnectionStore, try to register XPushConnectionInMemoryStore ..."); + connectionStore = new XPushConnectionInMemoryStore (); + services.AddSingleton (connectionStore); + } + + // + var webRtcConnectionStore = services.GetRegisteredService (); + if (webRtcConnectionStore.IsNull ()) { + // + Log ($"there is no provided XWebRTCConnectionStore, try to register XWebRTCConnectionInMemoryStore ..."); + webRtcConnectionStore = new XWebRTCConnectionInMemoryStore (); + services.AddSingleton (webRtcConnectionStore); + } + #endregion + + // + // Register SignalR ... + var builder = services.AddSignalR (); + + // + // Add NewtonSoftJson Protocol ... + builder.AddNewtonsoftJsonProtocol (x => { + x.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.PayloadSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver (); + }); + + // + // Register XPushServiceUserNameProvider ... + services.AddSingleton (); + + // + // add Message Protocol Support ... + if (config.AddSupportMessageProtocol) { + builder.AddMessagePackProtocol (); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Extensions/.gitkeep b/Extensions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Extensions/XWebRTCConnectionExtensions.cs b/Extensions/XWebRTCConnectionExtensions.cs new file mode 100644 index 0000000..a335871 --- /dev/null +++ b/Extensions/XWebRTCConnectionExtensions.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Linq; +using xCommons.Extensions; +using xPushService.Models; + +namespace xPushService.Extensions { + public static class XWebRTCConnectionExtensions { + /// + /// Validate Connection ... + /// + /// + /// + public static bool IsValid (this XWebRTCConnectionDto source) { + // + var result = false; + result = !source + .IsNull () && !source.CallerConnectionId + .IsNullOrEmpty () && ( + source.Callees + .HasChild () ? source.Callees + .All (callee => !callee.ConnectionId.IsNullOrDefault ()) : + true + ); + + // + return result; + } + + /// + /// Check a ConnectionId is Caller of a connection ... + /// + /// + /// + /// + public static bool IsCaller ( + this XWebRTCConnectionDto source, + string connectionId + ) { + // + var result = false; + + // + // Validate Args ... + if (!source.IsValid () || + connectionId.IsNullOrEmpty () + ) { + return result; + } + + // + result = connectionId + .ToNormalString () == source.CallerConnectionId + .ToNormalString (); + return result; + } + + /// + /// Check a ConnectionId is in Callees of a connection ... + /// + /// + /// + /// + public static bool IsInCallees ( + this XWebRTCConnectionDto source, + string connectionId + ) { + // + var result = false; + + // + // Validate Args ... + if (!source.IsValid () || + !source.Callees.HasChild () || + connectionId.IsNullOrEmpty () + ) { + return result; + } + + // + result = source.Callees.Any (callee => callee.ConnectionId + .ToNormalString () == connectionId + .ToNormalString ()); + + // + return result; + } + + /// + /// Get Receivers ... + /// + /// + /// + public static IReadOnlyList GetReceivers (this XWebRTCConnectionDto source) { + // + var result = new List (); + + // + // Validate Args ... + if (!source.IsValid ()) { + return result; + } + + // + // Add Caller ... + result.Add (source.CallerConnectionId); + + // + // Add Callees ... + source.Callees + .ToList () + .ForEach (callee => { + result.Add (callee.ConnectionId); + }); + + // + return result; + } + + /// + /// Retrieve Callee from Callees ... + /// + /// + /// + /// + public static XWebRTCCalleeDto GetCallee ( + this XWebRTCConnectionDto source, + string connectionId + ) { + // + // Validate Args ... + if (!source.IsValid () || + connectionId.IsNullOrEmpty () + ) { + return null; + } + + // + // Check Connection Id is In Callees or not ... + var isInCallees = source.IsInCallees (connectionId); + if (!isInCallees) { + return null; + } + + // + // retrieve result ... + var result = source.Callees + .FirstOrDefault (callee => callee.ConnectionId + .ToNormalString () == connectionId + .ToNormalString () + ); + + // + // return result ... + return result; + } + } +} \ No newline at end of file diff --git a/Helpers/XPushServiceHelper.cs b/Helpers/XPushServiceHelper.cs new file mode 100644 index 0000000..7b2d17a --- /dev/null +++ b/Helpers/XPushServiceHelper.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using xCommons.Extensions; +using xExceptions.Constants; +using xPushService.Base; +using xPushService.Models; + +namespace xPushService.Helpers { + public class XPushServiceHelper { + /// + /// a collection of Hubs for Registring ... + /// + /// + /// + private List hubs = new List (); + + /// + /// add specified hub to provided hubs for registration ... + /// + /// + /// + public void AddHub (string route) where TXHub : XBaseHub { + // + #region Validate Args ... + // + // check route not null ... + if ( + route.IsNullOrEmpty () || + route.Trim ().IsNullOrEmpty () + ) { + throw XException.InvalidArgs.ToException (); + } + + // + // Normalize Route ... + route = route.ToNormalString (); + + // + // check route and hub must be unique ... + var isRouteExists = hubs.Any (hd => hd.Route == route); + var isHubExists = hubs.Any (hd => hd.Hub == typeof (TXHub)); + if ( + isHubExists || + isRouteExists + ) { + throw XException.Duplicate.ToException (); + } + #endregion + + // + // Add Provided Hub to List ... + hubs.Add (new XHubDescriptor { + Route = route, + Hub = typeof (TXHub) + }); + } + + /// + /// remove an specific hub from provided list ... + /// + /// + public void RemoveHub () where TXHub : XBaseHub { + // + var existsHubDescriptor = hubs.SingleOrDefault (hd => hd.Hub == typeof (TXHub)); + if (!existsHubDescriptor.IsNull ()) { + hubs.Remove (existsHubDescriptor); + } + } + + /// + /// retrieve provided hubs count ... + /// + /// + public int Count () { + return hubs.Count; + } + + /// + /// retrieve all provided hubs for registration ... + /// + /// + public List GetHubs () { + return hubs; + } + } +} \ No newline at end of file diff --git a/Interfaces/IXAuthorizedPushActions.cs b/Interfaces/IXAuthorizedPushActions.cs new file mode 100644 index 0000000..e690982 --- /dev/null +++ b/Interfaces/IXAuthorizedPushActions.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXAuthorizedPushActions { + /// + /// an authenticated user can push a message to others ... + /// + /// + /// + Task UserMessageAsync (XPushMessage message); + + /// + /// an admin user can push a message to others ... + /// + /// + /// + Task AdminMessageAsync (XPushMessage message); + } +} \ No newline at end of file diff --git a/Interfaces/IXGroupsPushActions.cs b/Interfaces/IXGroupsPushActions.cs new file mode 100644 index 0000000..ac5ceba --- /dev/null +++ b/Interfaces/IXGroupsPushActions.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXGroupsPushActions { + /// + /// send a message to specific group ... + /// + /// + /// + /// + Task SendMessageToGroup ( + string groupName, + XPushMessage message + ); + + /// + /// send a message to specific groups ... + /// + /// + /// + /// + Task SendMessageToGroups ( + IEnumerable groupNames, + XPushMessage message + ); + + /// + /// add a connection to specific group ... + /// + /// + /// + /// + Task AddConnectionToGroup ( + string connectionId, + string groupName + ); + + /// + /// remove specific connection from a group ... + /// + /// + /// + /// + Task RemoveConnectionFromGroup ( + string connectionId, + string groupName + ); + + /// + /// retrieve all groups name ... + /// + /// + Task> GetGroupNames (); + + /// + /// retrieve all groups with it's connections ... + /// + /// + Task> GetGroups (); + + /// + /// remove all connections of specific group ... + /// + /// + /// + Task RemoveGroupConnections (string groupName); + + /// + /// retrieve all groups name for specific connection ... + /// + /// + /// + Task> GetConnectionGroups (string connectionId); + + /// + /// retrieve all connections in specific group ... + /// + /// + /// + Task> GetGroupConnections (string groupName); + } +} \ No newline at end of file diff --git a/Interfaces/IXPushConnectionStore.cs b/Interfaces/IXPushConnectionStore.cs new file mode 100644 index 0000000..1fe9405 --- /dev/null +++ b/Interfaces/IXPushConnectionStore.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using xModels.Dtos; +using xModels.Interfaces; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXPushConnectionStore : IXBaseStore { + // + #region Custom ... + /// + /// retrieve specific connection by it's Id ... + /// + /// + /// + Task GetByConnectionId (string connectionId); + + /// + /// retrieve specific user's connections ... + /// + /// + /// + Task> GetByUserName (string userName); + + /// + /// retrieve specific user's connected device connection + /// + /// + /// + /// + Task GetByUserDevice (string userName, XDeviceDto device); + + /// + /// check a connection exists or not by it's id ... + /// + /// + /// + Task IsExistsConnectionId (string connectionId); + + /// + /// check specific user connected or not ... + /// + /// + /// + Task IsExistsUser (string userName); + + /// + /// retrieve specific user's connected devices ... + /// + /// + /// + /// + Task> GetUserDevices (string userName); + + /// + /// remove specific connection by it's id ... + /// + /// + /// + Task RemoveByConnectionId (string connectionId); + + /// + /// remove specified user's connected device connection ... + /// + /// + /// + /// + Task RemoveByUserDevice (string userName, XDeviceDto device); + + /// + /// remove specific user's all connected devices ... + /// + /// + /// + Task RemoveUser (string userName); + + /// + /// Get specific connections last seen ... + /// + /// + /// + Task GetLastSeenByConnectionId (string connectionId); + + /// + /// get specific user's last seen ... + /// + /// + /// + Task GetLastSeenByUserName (string userName); + + /// + /// get specific user's connected device last seen ... + /// + /// + /// + /// + Task GetLastSeenByUserDevice (string userName, XDeviceDto device); + + /// + /// Update Connection last seen ... + /// + /// + /// + Task UpdateLastSeen(string connectionId); + #endregion + } +} \ No newline at end of file diff --git a/Interfaces/IXPushGroupStore.cs b/Interfaces/IXPushGroupStore.cs new file mode 100644 index 0000000..c25eb68 --- /dev/null +++ b/Interfaces/IXPushGroupStore.cs @@ -0,0 +1,6 @@ +using xModels.Interfaces; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXPushGroupStore : IXBaseStore { } +} \ No newline at end of file diff --git a/Interfaces/IXPushProvider.cs b/Interfaces/IXPushProvider.cs new file mode 100644 index 0000000..9b3cbe7 --- /dev/null +++ b/Interfaces/IXPushProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace xPushService.Interfaces +{ + public interface IXPushProvider + { + + } +} \ No newline at end of file diff --git a/Interfaces/IXUserPushActions.cs b/Interfaces/IXUserPushActions.cs new file mode 100644 index 0000000..1ce9f57 --- /dev/null +++ b/Interfaces/IXUserPushActions.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using xModels.Dtos; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXUserPushActions { + /// + /// add an specific user to a group ... + /// + /// + /// + /// + Task AddUserToGroup ( + string userName, + string groupName + ); + + /// + /// add an specific user's connected device to a group ... + /// + /// + /// + /// + /// + Task AddUserDeviceToGroup ( + string userName, + string groupName, + XDeviceDto device + ); + + /// + /// remove a user from specific groups ... + /// + /// + /// + /// + Task RemoveUserFromGroup ( + string userName, + string groupName + ); + + /// + /// remove specific user's connected device from a group ... + /// + /// + /// + /// + /// + Task RemoveUserDeviceFromGroup ( + string userName, + string groupName, + XDeviceDto device + ); + + /// + /// send a message to specific user ... + /// + /// + /// + /// + Task SendMessageToUser ( + string userName, + XPushMessage message + ); + + /// + /// send a message to a user's specific connected device ... + /// + /// + /// + /// + /// + Task SendMessageToUserDevice ( + string userName, + XPushMessage message, + XDeviceDto device + ); + + /// + /// send a message to specific users ... + /// + /// + /// + /// + Task SendMessageToUsers ( + IEnumerable userNames, + XPushMessage message + ); + + /// + /// retrieve all users groups ... + /// + /// + /// + Task> GetUserGroups (string userName); + + /// + /// retrieve specific user's connected device ... + /// + /// + /// + Task> GetUserConnections (string userName); + } +} \ No newline at end of file diff --git a/Interfaces/IXWebRTCConnectionStore.cs b/Interfaces/IXWebRTCConnectionStore.cs new file mode 100644 index 0000000..143932d --- /dev/null +++ b/Interfaces/IXWebRTCConnectionStore.cs @@ -0,0 +1,6 @@ +using xModels.Interfaces; +using xPushService.Models; + +namespace xPushService.Interfaces { + public interface IXWebRTCConnectionStore : IXBaseStore { } +} \ No newline at end of file diff --git a/Models/XHubDescriptor.cs b/Models/XHubDescriptor.cs new file mode 100644 index 0000000..a9cca27 --- /dev/null +++ b/Models/XHubDescriptor.cs @@ -0,0 +1,20 @@ +using System; + +namespace xPushService.Models { + /// + /// Describe a Hub to Register ... + /// + public class XHubDescriptor { + /// + /// Specified the Hub Route ... + /// + /// + public string Route { get; set; } + + /// + /// Specified the Hub Class Type ... + /// + /// + public Type Hub { get; set; } + } +} \ No newline at end of file diff --git a/Models/XPushConnectionDto.cs b/Models/XPushConnectionDto.cs new file mode 100644 index 0000000..e0411fb --- /dev/null +++ b/Models/XPushConnectionDto.cs @@ -0,0 +1,31 @@ +using System; +using xModels.Base; +using xModels.Dtos; + +namespace xPushService.Models { + /// + /// a Hub Connection ... + /// + public class XPushConnectionDto : XBaseStorableDto { + /// + /// Connection Id ... + /// + /// + public override string Id { get; set; } + /// + /// Connected User Name ... + /// + /// + public string User { get; set; } + /// + /// Connected User Device ... + /// + /// + public XDeviceDto Device { get; set; } + /// + /// Connected User's Last Seen ... + /// + /// + public DateTime LastSeen { get; set; } + } +} \ No newline at end of file diff --git a/Models/XPushGroupDto.cs b/Models/XPushGroupDto.cs new file mode 100644 index 0000000..7ed354e --- /dev/null +++ b/Models/XPushGroupDto.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using xModels.Base; + +namespace xPushService.Models { + public class XPushGroupDto : XBaseStorableDto { + public override string Id { get; set; } + public List Connections { get; set; } + } +} \ No newline at end of file diff --git a/Models/XPushMessage.cs b/Models/XPushMessage.cs new file mode 100644 index 0000000..93d6bfa --- /dev/null +++ b/Models/XPushMessage.cs @@ -0,0 +1,51 @@ +using System; +using xPushService.Constants; + +namespace xPushService.Models { + public class XPushMessage { + /// + /// Specify the Type of Push Notification ... + /// + /// + public XPushType Type { get; set; } + + /// + /// Specify the Topic of PushNotification ... + /// NOTE: XPushTopic enum contains default Values and User can Extends it ... + /// + /// + public int Topic { get; set; } + + /// + /// Specify the Action Name by passing String Values ... + /// NOTE: XPushAction enum contains default Values for Actions ... + /// + /// + public string Action { get; set; } + + /// + /// if this is a User Push Action, we had to pass Actor ... + /// + /// + public string Actor { get; set; } + + /// + /// in CRUD actions or some Custom Actions, we can pass propper data + /// to clients ... + /// + /// + public string Payload { get; set; } + + /// + /// the message or additional Informations for passing to client ... + /// + /// + public string Message { get; set; } + + /// + /// Push notification date and time ... + /// + /// + public DateTime TimeStamp { get; set; } + } +} \ No newline at end of file diff --git a/Models/XWebRTCCalleeDto.cs b/Models/XWebRTCCalleeDto.cs new file mode 100644 index 0000000..b881b9d --- /dev/null +++ b/Models/XWebRTCCalleeDto.cs @@ -0,0 +1,34 @@ +using System; +using xModels.Base; +using xModels.Dtos; +using xPushService.Constants; + +namespace xPushService.Models { + public class XWebRTCCalleeDto : XBaseDto { + /// + /// Callee User Name ... + /// + /// + public string User { get; set; } + /// + /// Callee Device ... + /// + /// + public XDeviceDto Device { get; set; } + /// + /// Calee Connection Id ... + /// + /// + public string ConnectionId { get; set; } + /// + /// call end time ... + /// + /// + public DateTime EndTime { get; set; } + /// + /// call end reason ... + /// + /// + public XCallEndReason EndReason { get; set; } + } +} \ No newline at end of file diff --git a/Models/XWebRTCConnectionDto.cs b/Models/XWebRTCConnectionDto.cs new file mode 100644 index 0000000..d2c5af5 --- /dev/null +++ b/Models/XWebRTCConnectionDto.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using xModels.Base; +using xModels.Dtos; +using xPushService.Constants; + +namespace xPushService.Models { + /// + /// describe a WebRTC Call ... + /// + public class XWebRTCConnectionDto : XBaseStorableDto { + public override string Id { get; set; } + /// + /// Call Type ... + /// + /// + public XCallType Type { get; set; } + /// + /// call request Time ... + /// + /// + public DateTime RequestTime { get; set; } + /// + /// Payload object ... + /// + /// + public object Payload { get; set; } + /// + /// Caller User Name ... + /// + /// + public string CallerUser { get; set; } + /// + /// Caller Device ... + /// + /// + public XDeviceDto CallerDevice { get; set; } + /// + /// Caller Connection Id ... + /// + /// + public string CallerConnectionId { get; set; } + /// + /// Callees Informations ... + /// + /// + public IEnumerable Callees { get; set; } + } +} \ No newline at end of file diff --git a/Models/XWebRTCSignalDto.cs b/Models/XWebRTCSignalDto.cs new file mode 100644 index 0000000..4f8b966 --- /dev/null +++ b/Models/XWebRTCSignalDto.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using xModels.Base; + +namespace xPushService.Models { + public class XWebRTCSignalDto : XBaseDto { + /// + /// Signalign Offer ... + /// + /// + public object Offer { get; set; } + /// + /// Signaling Answer ... + /// + /// + public object Answer { get; set; } + /// + /// Signaling Candidates + /// + /// + public IEnumerable Cadidates { get; set; } + } +} \ No newline at end of file diff --git a/Policies/XPushServiceConnectionRetryPolicy.cs b/Policies/XPushServiceConnectionRetryPolicy.cs new file mode 100644 index 0000000..94320e7 --- /dev/null +++ b/Policies/XPushServiceConnectionRetryPolicy.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.AspNetCore.SignalR.Client; +using xPushService.Configurations; + +namespace xPushService.Policies { + public class XPushServiceConnectionRetryPolicy : IRetryPolicy { + // + #region Props ... + // + Exception retryReason = null; + long previousRetryCount = 0; + TimeSpan elapsedMilliseconds; + + // + private readonly XPushServiceConfiguration config; + #endregion + + // + #region Constructor ... + public XPushServiceConnectionRetryPolicy ( + XPushServiceConfiguration config + ) { + this.config = config; + } + #endregion + + public TimeSpan? NextRetryDelay (RetryContext retryContext) { + // + this.retryReason = retryContext.RetryReason; + this.previousRetryCount = retryContext.PreviousRetryCount; + this.elapsedMilliseconds = retryContext.ElapsedTime; + + // + if (retryContext.PreviousRetryCount < config.PushConnectionMaxRetry) { + return TimeSpan.FromMilliseconds (this.config.PushConnectionReconnectDelay); + } else { + return null; + } + } + } +} \ No newline at end of file diff --git a/Providers/XPushProvider.cs b/Providers/XPushProvider.cs new file mode 100644 index 0000000..c9b8879 --- /dev/null +++ b/Providers/XPushProvider.cs @@ -0,0 +1,5 @@ +using xPushService.Interfaces; + +namespace xPushService.Providers { + public class XPushProvider : IXPushProvider { } +} \ No newline at end of file diff --git a/Providers/XPushServiceUserNameProvider.cs b/Providers/XPushServiceUserNameProvider.cs new file mode 100644 index 0000000..f76850f --- /dev/null +++ b/Providers/XPushServiceUserNameProvider.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; + +namespace xPushService.Providers { + public class XPushServiceUserNameProvider : IUserIdProvider { + public string GetUserId (HubConnectionContext connection) { + // + var result = connection.User?.Identity?.Name; + return result; + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..92e01fd --- /dev/null +++ b/README.md @@ -0,0 +1,1119 @@ +# xPushService + +it is a Part of xDashboard Projects on SaherElm IT Center which provides all required models and actions in related to handling SignalR Push Platforms. + +since this module used as a base infrastructure provider, you have to create a Helper Module based on it, for creating your projects requirements. we call it commonly **xPushHelper**. + +## xPushHelper + +it is a simple **xDashboard** based module. which depends on these modules: + +- xCommons +- xPushService + +this is so important, if you protect your data using **xIdentityServer**. you need to add an additional refrence to your **xIdentityHelper** module. + +```c# + + + + + + + + + + +``` + +after adding dependencies. you have to extends your PushHelper module. + +by following **SignalR** rules, for each topics you have to add an specific **HUB** and register it in your **DI**. + +since all important actions for handling communications between connected peers must be handled in Hubs. at the first step you have to create a Base Class for all of your Hubs. + +in this sample since we need to have Authorized HUB Actions, we add IdentityService as dependency to Helper Module, now next step is implement a BaseHub class which supports Authorized Actions based on our User Roles. + +## XPushBaseHub + +```c# + public abstract class XPushBaseHub : XBaseHub, IXAuthorizedPushActions, IXUserPushActions, IXGroupsPushActions { + // + #region Constructor ... + protected XPushBaseHub ( + IXPushGroupStore groupStore, + IXPushConnectionStore connectionStore + ) : base (groupStore, connectionStore) { } + #endregion + + // + #region Authorized Push Actions ... + /// + /// an authenticated user can push a message to others ... + /// + /// + /// + [Authorize (Policy = XPolicies.User)] + public Task UserMessageAsync (XPushMessage message) { + // + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients.Others.SendAsync (XBasePushAction.UserMessage.GetStringValue (), message); + } + + /// + /// an admin user can push a message to others ... + /// + /// + /// + [Authorize (Policy = XPolicies.Admin)] + public Task AdminMessageAsync (XPushMessage message) { + // + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + return Clients.Others.SendAsync (XBasePushAction.AdminMessage.GetStringValue (), message); + } + #endregion + + // + #region User Push Actions ... + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task AddUserToGroup (string userName, string groupName) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + var group = GetGroup (groupName) + .RunTask (); + + // + var connections = connectionStore + .GetByUserName (userName) + .RunTask (); + var mustAddConnections = connections + .Where (c => !group.Connections + .Any (gc => gc == c.Id)); + if (!mustAddConnections.HasChild ()) { + return result; + } + + // + // Add all new Connections to Group ... + var allUpdate = false; + mustAddConnections.ToList ().ForEach (c => { + // + try { + // + Groups + .AddToGroupAsync (c.Id, groupName) + .RunTask (); + + // + group.Connections.Add (c.Id); + + // + allUpdate = allUpdate || true; + } catch { + allUpdate = allUpdate || false; + } + }); + if (!allUpdate) { + return result; + } + + // + result = AddOrUpdateGroup (group); + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task AddUserDeviceToGroup (string userName, string groupName, XDeviceDto device) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if ( + device.IsNull () || + userName.IsNullOrEmpty () || + groupName.IsNullOrEmpty () + ) { + return result; + } + + // + // Find User Connection ... + var userConnection = connectionStore + .GetByUserDevice ( + device: device, + userName: userName + ) + .RunTask (); + if (userConnection.IsNull ()) { + return result; + } + + // + // Get Group ... + var group = GetGroup (groupName) + .RunTask (); + + // + // check if connection exists in group ... + if (group.Connections.Any (c => c == userConnection.Id)) { + return result; + } + + // + try { + // + Groups + .AddToGroupAsync ( + userConnection.Id, + groupName + ) + .RunTask (); + + // + group.Connections.Add (userConnection.Id); + } catch { + return result; + } + + // + result = AddOrUpdateGroup (group); + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task RemoveUserFromGroup (string userName, string groupName) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if ( + userName.IsNullOrEmpty () || + groupName.IsNullOrEmpty () + ) { + return result; + } + + // + // Check Group Exists or not ... + var isGroupExists = groupStore + .IsExistsByKey (groupName) + .RunTask (); + if (!isGroupExists) { + return result; + } + + // + // Retrieve User Connections ... + var userConnections = connectionStore + .GetByUserName (userName) + .RunTask (); + if (!userConnections.HasChild ()) { + return result; + } + + // + var group = GetGroup (groupName) + .RunTask (); + var mustRemoveConnections = userConnections + .Where (c => group.Connections + .Any (gcId => c.Id == gcId)); + if (!mustRemoveConnections.HasChild ()) { + return result; + } + + // + // try to remove connections from group ... + try { + // + mustRemoveConnections + .ToList () + .ForEach (c => { + // + var isRemoved = connectionStore + .RemoveByConnectionId (c.Id) + .RunTask (); + if (isRemoved) { + Groups.RemoveFromGroupAsync ( + c.Id, + groupName + ) + .RunTask (); + } + }); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task RemoveUserDeviceFromGroup (string userName, string groupName, XDeviceDto device) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Arg ... + if ( + device.IsNull () || + userName.IsNullOrEmpty () || + groupName.IsNullOrEmpty () + ) { + return result; + } + + // + // Check group exists ... + var isGroupExists = groupStore + .IsExistsByKey (groupName) + .RunTask (); + if (!isGroupExists) { + return result; + } + + // + var group = GetGroup (groupName) + .RunTask (); + if (!group.Connections.HasChild ()) { + return result; + } + + // + // retrieve user connection ... + var userDeviceConnection = connectionStore + .GetByUserDevice ( + device: device, + userName: userName + ) + .RunTask (); + if ( + userDeviceConnection.IsNull () || + !group.Connections.Any (gc => gc != userDeviceConnection.Id) + ) { + return result; + } + + // + try { + // + Groups.RemoveFromGroupAsync ( + groupName: groupName, + connectionId: userDeviceConnection.Id + ); + } catch { + return result; + } + group.Connections.Remove (userDeviceConnection.Id); + + // + result = AddOrUpdateGroup (group); + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledUser)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task SendMessageToUser (string userName, XPushMessage message) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if (userName.IsNullOrEmpty ()) { + return result; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + // Check user exists ... + var userConnections = connectionStore + .GetByUserName (userName) + .RunTask (); + if (!userConnections.HasChild ()) { + return result; + } + + // + try { + // + Clients + .User (userName) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ) + .RunTask(); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledUser)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task SendMessageToUserDevice (string userName, XPushMessage message, XDeviceDto device) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if ( + device.IsNull () || + userName.IsNullOrEmpty () + ) { + return result; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + // Check user exists ... + var userDeviceConnection = connectionStore + .GetByUserDevice ( + device: device, + userName: userName + ) + .RunTask (); + if (userDeviceConnection.IsNull ()) { + return result; + } + + // + try { + // + Clients + .Client (userDeviceConnection.Id) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ) + .RunTask(); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledUser)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task SendMessageToUsers (IEnumerable userNames, XPushMessage message) { + // + // Temp result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if (!userNames.HasChild ()) { + return result; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + // Check user exists ... + var usersConnections = connectionStore + .FindMany (c => userNames.Contains (c.User)) + .RunTask (); + if (!usersConnections.HasChild ()) { + return result; + } + + // + try { + // + Clients + .Users ( + userNames + .ToList () + .AsReadOnly () + ) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ) + .RunTask(); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledUser)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetUserGroups (string userName) { + // + // Temp Result ... + var resultList = new List (); + + // + // Validate Args ... + if (userName.IsNullOrEmpty ()) { + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + // + // Retrieve User Connections ... + var userConnections = connectionStore + .GetByUserName (userName) + .RunTask (); + if (!userConnections.HasChild ()) { + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + // + // Retrieve user Groups ... + var userGroups = groupStore + .FindMany (pg => pg.Connections + .Any (pgCId => userConnections + .Any (uc => uc.Id == pgCId))) + .RunTask (); + if (!userGroups.HasChild ()) { + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + // + // prepare result ... + resultList = userGroups + .Select (ug => ug.Id) + .ToList (); + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetUserConnections (string userName) { + // + var resultList = new List (); + + // + // Validate Args ... + if (userName.IsNullOrEmpty ()) { + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + // + // check current user is Admin or not ... + var connectedUser = GetUserName (); + var connectedUserRole = GetUserRole (); + var isAdminOrAgent = connectedUserRole.ToNormalString () == "admin" || + connectedUserRole.ToNormalString () == "agent"; + var isUserSame = connectedUser.ToNormalString () == userName.ToNormalString (); + + // + // check current user ... + if (!isAdminOrAgent && !isUserSame) { + return Task.FromResult ( + resultList + .AsEnumerable () + ); + } + + // + return connectionStore + .GetByUserName (userName); + } + #endregion + + // + #region Group Push Actions ... + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task SendMessageToGroup (string groupName, XPushMessage message) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if (groupName.IsNullOrEmpty ()) { + return result; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + // Check Group Exists ... + var isGroupExists = groupStore + .IsExistsByKey (groupName) + .RunTask (); + if (!isGroupExists) { + return result; + } + + // + try { + // + Clients + .Group (groupName) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ) + .RunTask(); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task SendMessageToGroups (IEnumerable groupNames, XPushMessage message) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if (!groupNames.HasChild ()) { + return result; + } + + // + // Normalize Message ... + if (message.IsNull ()) { + message = GetPushMessage (); + } else { + message.TimeStamp = DateTime.UtcNow; + } + + // + // Check Group Exists ... + var isGroupsExists = Task.WhenAll (groupNames + .Select (gName => groupStore + .IsExistsByKey (gName))) + .RunTask (); + if (!isGroupsExists.All (isExists => !!isExists)) { + return result; + } + + // + try { + // + Clients + .Groups ( + groupNames + .ToList () + .AsReadOnly () + ) + .SendAsync ( + XBasePushAction.PushMessage.GetStringValue (), + message + ) + .RunTask(); + + // + result = Task.FromResult (true); + } catch { } + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task AddConnectionToGroup (string connectionId, string groupName) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if ( + groupName.IsNullOrEmpty () || + connectionId.IsNullOrEmpty () + ) { + return result; + } + + // + // Check Connection Exists ... + var isConnectionExists = connectionStore + .IsExistsConnectionId (connectionId) + .RunTask (); + if (!isConnectionExists) { + return result; + } + + // + // Get Normalized Group ... + var group = GetGroup (groupName) + .RunTask (); + var isConnectionInGroup = group.Connections.Any (cId => cId == connectionId); + if (isConnectionInGroup) { + return result; + } + + // + // add connection to group ... + group.Connections.Add (connectionId); + + // + result = AddOrUpdateGroup (group); + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task RemoveConnectionFromGroup (string connectionId, string groupName) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if ( + groupName.IsNullOrEmpty () || + connectionId.IsNullOrEmpty () + ) { + return result; + } + + // + // Check Connection Exists ... + var isConnectionExists = connectionStore + .IsExistsConnectionId (connectionId) + .RunTask (); + if (!isConnectionExists) { + return result; + } + + // + // Check Group Exists ... + var isGroupExists = groupStore + .IsExistsByKey (groupName) + .RunTask (); + if (!isGroupExists) { + return result; + } + + // + // Get Normalized Group ... + var group = GetGroup (groupName) + .RunTask (); + var isConnectionInGroup = group.Connections.Any (cId => cId == connectionId); + if (!isConnectionInGroup) { + return result; + } + + // + // add connection to group ... + group.Connections.Remove (connectionId); + + // + result = AddOrUpdateGroup (group); + + // + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetGroupNames () { + // + var groups = groupStore + .GetAll () + .RunTask (); + + // + var result = groups.Select (g => g.Id); + return Task.FromResult (result); + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetGroups () { + return groupStore.GetAll (); + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task RemoveGroupConnections (string groupName) { + // + // Empty result ... + var result = Task.FromResult (false); + + // + // Validate Args ... + if (groupName.IsNullOrEmpty ()) { + return result; + } + + // + // retrieve normalized group ... + var group = GetGroup (groupName) + .RunTask (); + if (!group.Connections.HasChild ()) { + return result; + } + + // + var allResult = false; + group.Connections + .ToList () + .ForEach (cIde => { + // + var isRemoved = connectionStore + .RemoveByConnectionId (cIde) + .RunTask (); + + // + if (isRemoved) { + group.Connections.Remove (cIde); + } + + // + allResult = allResult || isRemoved; + }); + if (!allResult) { + return result; + } + + // + result = Task.FromResult (true); + return result; + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetConnectionGroups (string connectionId) { + // + // Validate Args ... + if (connectionId.IsNullOrEmpty ()) { + return Task.FromResult ( + new List () + .AsEnumerable () + ); + } + + // + var connectionGroups = groupStore + .FindMany (group => group.Connections + .Contains (connectionId)) + .RunTask (); + + // + var resultList = connectionGroups + .Select (group => group.Id); + + // + return Task.FromResult (resultList); + } + + [Authorize (Policy = XPolicies.Admin)] + [Authorize (Policy = XPolicies.EnabledAgent)] + public Task> GetGroupConnections (string groupName) { + // + // Validate Args ... + if (groupName.IsNullOrEmpty ()) { + return Task.FromResult ( + new List () + .AsEnumerable () + ); + } + + // + var group = GetGroup (groupName) + .RunTask (); + + // + var resultList = connectionStore + .FindMany (c => group.Connections.Contains (c.Id)) + .RunTask (); + + // + return Task.FromResult (resultList); + } + #endregion + + // + #region Private ... + private Task GetGroup (string groupName) { + // + XPushGroupDto result = null; + // + // Validate Args ... + if (groupName.IsNullOrEmpty ()) { + return Task.FromResult (result); + } + + // + var isExistsGroup = groupStore + .IsExistsByKey (groupName) + .RunTask (); + + // + if (!isExistsGroup) { + result = new XPushGroupDto { + Id = groupName, + Connections = new List () + }; + } else { + result = groupStore + .Get (groupName) + .RunTask (); + } + + // + return Task.FromResult (result); + } + + private Task AddOrUpdateGroup (XPushGroupDto group) { + // + var result = false; + + // + // Validate Args ... + if (group.IsNull ()) { + return Task.FromResult (result); + } + + // + var isExistsGroup = groupStore + .IsExistsByKey (group.Id) + .RunTask (); + if (!isExistsGroup) { + result = groupStore + .Add (group) + .RunTask (); + } else { + // + var updatedGroup = groupStore + .Update (group) + .RunTask (); + result = !updatedGroup.IsNull (); + } + + // + return Task.FromResult (result); + } + #endregion + } +``` + +one of most important things which you had to do in **xPushHelper** is the implementation of your **HUB**(s). + +for this, you had to use **XBaseHub** class and extends your Hubs from it. here for example we create a ViewHub as follow: + +## XViewHub + +```c# + public class XViewHub : XBaseHub { + // + #region Props ... + public static int Count = 0; + #endregion + + // + #region Constructor ... + public XViewHub ( + IXPushGroupStore groupStore, + IXPushConnectionStore connectionStore + ) : base (groupStore, connectionStore) { } + #endregion + + // + #region Actions ... + [Authorize] + public Task NotifyCount () { + return Clients.All.SendAsync ("NotifyCount", Count); + } + + public Task PublicNotifyCount () { + return Clients.All.SendAsync ("NotifyCount", Count); + } + + public int IncreaseCount () { + // + Count += 1; + return Count; + } + #endregion + } +``` + +## Handling WebRTC connections + +there is a Base Class **XBaseWebRTCHub** for enable WebRTC on your projects. all thing you had to do is to extends a class from it. + +### XWebRTCHub + +as you can see below, there is not anything to add to this class, all things handled by Base Class by default. but if there is some custom actions you need, add them here ... + +```c# + public class XWebRTCHub : XBaseWebRTCHub { + // + #region Constructor ... + public XWebRTCHub ( + IXPushGroupStore groupStore, + IXPushConnectionStore connectionStore, + IXWebRTCConnectionStore webRTCConnectionStore + ) : base (groupStore, connectionStore, webRTCConnectionStore) { } + #endregion + } +``` + +after complete all requirements inside **xPushHelper** module, next step is to register **xPushService** in your application **DI**, which happens on **Startup.cs** file. + +**NOTE:** if you want to use Authorization and Authentication inside your Hubs, you had to register xPushService after enabling Authorization. + +before moving forward, please configure **xPushService** as follow, in your appSettings.json file: + +```json + + ... + + "PushServiceConfiguration": { + "BaseRoute": "hubs", + "AddSupportMessageProtocol": false + }, + + ... + +``` + +this is a sample configuration. for more info you can review [xPushService Documentation](http://x-dashboard.saherelm.ir/modules/xPushService/index.html#File:Configurations/XPushServiceConfiguration.cs) + +then register **xPushService** in **Startup.cs**: + +```c# + public class Startup { + ... + public void ConfigureServices (IServiceCollection services) { + ... + // + // Register XPushService ... + services.AddXPushService (Configuration); + ... + } + ... + } +``` + +next step is use **xPushService** middleware for declare and usage of your custom created **HUB**(s) in **xPushHelper** module. this must be done by an instance of **XPushServiceHelper** class which you have to use it for registering your hubs. then pass this to **xPushService** Middleware for configure them for working. + +```c# + public class Startup { + ... + public void Configure (IApplicationBuilder app, IWebHostEnvironment env) { + ... + // + #region XPushService ... + // + var helper = new XPushServiceHelper (); + + // + helper.AddHub ("view"); + helper.AddHub ("webrtc"); + + // + app.UseXPushService (helper); + #endregion + ... + } + ... + } +``` + +for the moment **xPushService** configured successfully and activated in your application. + +for client side, you can follow it's related documentation. + +## Maintainer + +Hadi Khazaee asl + +[https://www.saherelm.ir](https://www.saherelm.ir) + +[hadi_khazaee_asl@yahoo.com](mailto:hadi_khazaee_asl@yahoo.com) diff --git a/Store/XPushConnectionInMemoryStore.cs b/Store/XPushConnectionInMemoryStore.cs new file mode 100644 index 0000000..ca0ec36 --- /dev/null +++ b/Store/XPushConnectionInMemoryStore.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using xCommons.Extensions; +using xExceptions.Constants; +using xModels.Base; +using xModels.Dtos; +using xModels.Providers; +using xPushService.Interfaces; +using xPushService.Models; + +namespace xPushService.Store { + public class XPushConnectionInMemoryStore : XBaseInMemoryStore, IXPushConnectionStore { + // + #region Custom ... + /// + /// retrieve specific connection by it's Id ... + /// + /// + /// + public Task GetByConnectionId (string connectionId) { + return Get (connectionId); + } + + /// + /// retrieve specific user's connections ... + /// + /// + /// + public Task> GetByUserName (string userName) { + return FindMany (c => c.User.ToNormalString () == userName.ToNormalString ()); + } + + /// + /// retrieve specific user's connected device connection + /// + /// + /// + /// + public Task GetByUserDevice (string userName, XDeviceDto device) { + return FindOne (c => + c.User.ToNormalString () == userName.ToNormalString () && + c.Device.IsSameContent ( + device, null, null, null, false + ) + ); + } + + /// + /// check a connection exists or not by it's id ... + /// + /// + /// + public Task IsExistsConnectionId (string connectionId) { + return IsExistsByKey (connectionId); + } + + /// + /// check specific user connected or not ... + /// + /// + /// + public Task IsExistsUser (string userName) { + return GetByUserName (userName) + .ContinueWith (findTask => { + // + var findResult = findTask + .RunTask(); + + // + var result = findResult.Any (); + return result; + }); + } + + /// + /// retrieve specific user's connected devices ... + /// + /// + /// + /// + public Task> GetUserDevices (string userName) { + return GetByUserName (userName) + .ContinueWith (userConnectionsTask => { + // + var userConnections = userConnectionsTask + .RunTask(); + + // + var result = userConnections.Select (c => c.Device); + return result; + }); + } + + /// + /// remove specific connection by it's id ... + /// + /// + /// + public Task RemoveByConnectionId (string connectionId) { + return RemoveByKey (connectionId); + } + + /// + /// remove specified user's connected device connection ... + /// + /// + /// + /// + public Task RemoveByUserDevice (string userName, XDeviceDto device) { + return GetByUserDevice ( + device: device, + userName: userName + ) + .ContinueWith (connectionTask => { + // + var connection = connectionTask + .RunTask(); + + // + if (connection.IsNull ()) { + return false; + } + + // + var result = Remove (connection) + .RunTask(); + return result; + }); + } + + /// + /// remove specific user's all connected devices ... + /// + /// + /// + public Task RemoveUser (string userName) { + return GetByUserName (userName) + .ContinueWith (connectionsTask => { + // + var connections = connectionsTask + .RunTask(); + + // + if (!connections.HasChild ()) { + return false; + } + + // + var result = RemoveMany (new XBaseRangeRequest { + Items = connections + }) + .RunTask(); + + // + return result; + }); + } + + /// + /// Get specific connections last seen ... + /// + /// + /// + public Task GetLastSeenByConnectionId (string connectionId) { + return GetByConnectionId (connectionId) + .ContinueWith (connectionTask => { + // + var connection = connectionTask + .RunTask(); + + // + if (connection.IsNull ()) { + throw XException.NotFound.ToException (); + } + + // + var result = connection.LastSeen; + return result; + }); + } + + /// + /// get specific user's last seen ... + /// + /// + /// + public Task GetLastSeenByUserName (string userName) { + return GetByUserName (userName) + .ContinueWith (connectionsTask => { + // + var connections = connectionsTask + .RunTask (); + + // + if (!connections.HasChild ()) { + throw XException.NotFound.ToException (); + } + + // + var result = connections + .Select (c => c.LastSeen) + .Max (); + + // + return result; + }); + } + + /// + /// get specific user's connected device last seen ... + /// + /// + /// + /// + public Task GetLastSeenByUserDevice (string userName, XDeviceDto device) { + return GetByUserDevice ( + device: device, + userName: userName + ) + .ContinueWith (connectionTask => { + // + var connection = connectionTask + .RunTask (); + + // + if (connection.IsNull ()) { + throw XException.NotFound.ToException (); + } + + // + var result = connection.LastSeen; + return result; + }); + } + + /// + /// Update Connection last seen ... + /// + /// + /// + public Task UpdateLastSeen (string connectionId) { + return IsExistsConnectionId (connectionId) + .ContinueWith (isExistsTask => { + // + // define empty result ... + var result = false; + + // + // run task and get result ... + result = isExistsTask + .RunTask(); + + // + // check result is false ... + if (!result) { + return result; + } + + // + // retreive connection ... + var connection = GetByConnectionId (connectionId) + .RunTask(); + result = !connection.IsNull (); + if (!result) { + return result; + } + + // + // Update Connection last seen ... + connection.LastSeen = DateTime.UtcNow; + var updatedConnection = Update (connection) + .RunTask (); + result = !updatedConnection.IsNull (); + + // + return result; + }); + } + #endregion + } +} \ No newline at end of file diff --git a/Store/XPushGroupInMemoryStore.cs b/Store/XPushGroupInMemoryStore.cs new file mode 100644 index 0000000..60712cc --- /dev/null +++ b/Store/XPushGroupInMemoryStore.cs @@ -0,0 +1,7 @@ +using xModels.Providers; +using xPushService.Interfaces; +using xPushService.Models; + +namespace xPushService.Store { + public class XPushGroupInMemoryStore : XBaseInMemoryStore, IXPushGroupStore { } +} \ No newline at end of file diff --git a/Store/XWebRTCConnectionInMemoryStore.cs b/Store/XWebRTCConnectionInMemoryStore.cs new file mode 100644 index 0000000..02dd98f --- /dev/null +++ b/Store/XWebRTCConnectionInMemoryStore.cs @@ -0,0 +1,7 @@ +using xModels.Providers; +using xPushService.Interfaces; +using xPushService.Models; + +namespace xPushService.Store { + public class XWebRTCConnectionInMemoryStore : XBaseInMemoryStore, IXWebRTCConnectionStore { } +} \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..632defe --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/xPushService.csproj b/xPushService.csproj new file mode 100644 index 0000000..b43cd7c --- /dev/null +++ b/xPushService.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + xDashboard.xPushService + 1.0.0 + Hadi Khazaee Asl + SaherElm IT Center + + it is a Part of xDashboard Projects on SaherElm IT Center which provides all required models + and actions in related to + handling SignalR Push Platforms. + + + + icon.png + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file