From 8d1f99593b43fc201eb6af6a7648b82b24888307 Mon Sep 17 00:00:00 2001
From: esantangelo <enzo020895@gmail.com>
Date: Wed, 28 Oct 2020 21:42:42 -0300
Subject: [PATCH] commit

---
 Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs          | 149 ++++++++++++++++++
 Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs      |  41 +++++
 Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs  |  72 +++++++++
 Tsi1.Api/Tsi1.Api/Startup.cs                  |   2 +
 .../Interfaces/IChatService.cs                |  18 +++
 .../Services/ChatService.cs                   |  61 +++++++
 .../Tsi1.DataLayer/Entities/Connection.cs     |  13 ++
 Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs     |  17 ++
 .../ConnectionConfiguration.cs                |  24 +++
 .../EntityConfiguration/GroupConfiguration.cs |  17 ++
 Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs        |   4 +
 11 files changed, 418 insertions(+)
 create mode 100644 Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs
 create mode 100644 Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs
 create mode 100644 Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs
 create mode 100644 Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs
 create mode 100644 Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs
 create mode 100644 Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs
 create mode 100644 Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs
 create mode 100644 Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs
 create mode 100644 Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs

diff --git a/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs b/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs
new file mode 100644
index 0000000..18897e2
--- /dev/null
+++ b/Tsi1.Api/Tsi1.Api/SignalR/ChatHub.cs
@@ -0,0 +1,149 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Tsi1.BusinessLayer.Dtos;
+using Tsi1.BusinessLayer.Interfaces;
+using Tsi1.DataLayer;
+using Tsi1.DataLayer.Entities;
+
+namespace Tsi1.Api.SignalR
+{
+    public class ChatHub : Hub
+    {
+        private readonly IMapper _mapper;
+        private readonly IHubContext<PresenceHub> _presenceHub;
+        private readonly PresenceTracker _tracker;
+        private readonly IMessageService _messageService;
+        private readonly IChatService _chatService;
+
+        public ChatHub(IMapper mapper, IHubContext<PresenceHub> presenceHub,
+            PresenceTracker tracker, IMessageService messageService, IChatService chatService)
+        {
+            _tracker = tracker;
+            _presenceHub = presenceHub;
+            _mapper = mapper;
+            _messageService = messageService;
+            _chatService = chatService
+
+        }
+
+        public override async Task OnConnectedAsync()
+        {
+            var httpContext = Context.GetHttpContext();
+            //var otherUser = httpContext.Request.Query["user"].ToString();
+            var otherUserId = int.Parse(httpContext.Request.Query["id"].ToString());
+
+            //var userName = Context.User.FindFirst("Username").Value;
+
+            var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
+            var tenantId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "TenantId").Value);
+
+
+            var groupName = GetGroupName(userId, otherUserId);
+            await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
+
+            await AddToGroup(groupName);
+
+            var messages = await _messageService.GetMessages(userId, otherUserId, tenantId);
+
+            await Clients.Caller.SendAsync("ReceiveMessages", messages);
+        }
+
+        public override async Task OnDisconnectedAsync(Exception exception)
+        {
+
+            await RemoveFromGroup();
+
+            await base.OnDisconnectedAsync(exception);
+        }
+
+        public async Task SendMessage(MessageCreateDto newMessage)
+        {
+            var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
+            var tenantId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "TenantId").Value);
+
+            if (userId == newMessage.ReceiverId)
+                throw new HubException("You cannot send messages to yourself");
+
+            var groupName = GetGroupName(userId, newMessage.ReceiverId);
+
+            var group = await _chatService.GetGroupByName(groupName);
+
+            if (group == null)
+            {
+                throw new HubException($"No existe el grupo {groupName}");
+            }
+
+            if (!group.Connections.Any(x => x.UserId == newMessage.ReceiverId))
+            {
+                var connections = await _tracker.GetConnectionsForUser(newMessage.ReceiverId);
+                if (connections != null)
+                {
+                    await _presenceHub.Clients.Clients(connections).SendAsync("NewMessageReceived", userId);
+                }
+            }
+
+            newMessage.SenderId = userId;
+            var messageDto = await _messageService.Send(newMessage);
+
+            await Clients.Group(groupName).SendAsync("NewMessage", messageDto);
+
+        }
+
+        private async Task<Group> AddToGroup(string groupName)
+        {
+            var group = await _chatService.GetGroupByName(groupName);
+
+            if (group == null)
+            {
+                throw new HubException($"No existe el grupo {groupName}");
+            }
+
+            var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
+
+            var connection = new Connection
+            {
+                ConnectionId = Context.ConnectionId,
+                UserId = userId,
+            };
+
+            if (group == null)
+            {
+                group = new Group
+                {
+                    Name = groupName,
+                };
+            }
+
+            group.Connections.Add(connection);
+
+            var result = await _chatService.CreateGroup(group);
+
+            if (result.HasError)
+            {
+                throw new HubException($"Error al crear el grupo {group}");
+            }
+
+            return group;
+        }
+
+        private async Task<bool> RemoveFromGroup()
+        {
+            await _chatService.RemoveConnectionFromGroup(Context.ConnectionId);
+
+            return true;
+        }
+
+        private string GetGroupName(int callerId, int otherId)
+        {
+            var compare = callerId < otherId;
+            return compare ? $"{callerId}-{otherId}" : $"{otherId}-{callerId}";
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs b/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs
new file mode 100644
index 0000000..e606bc9
--- /dev/null
+++ b/Tsi1.Api/Tsi1.Api/SignalR/PresenceHub.cs
@@ -0,0 +1,41 @@
+using Microsoft.AspNetCore.SignalR;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Tsi1.Api.SignalR
+{
+    public class PresenceHub : Hub
+    {
+        private readonly PresenceTracker _tracker;
+        public PresenceHub(PresenceTracker tracker)
+        {
+            _tracker = tracker;
+        }
+
+        public override async Task OnConnectedAsync()
+        {
+            var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
+
+            var isOnline = await _tracker.UserConnected(userId, Context.ConnectionId);
+            if (isOnline)
+                await Clients.Others.SendAsync("UserIsOnline", userId);
+
+            var currentUsers = await _tracker.GetOnlineUsers();
+            await Clients.Caller.SendAsync("GetOnlineUsers", currentUsers);
+        }
+
+        public override async Task OnDisconnectedAsync(Exception exception)
+        {
+            var userId = int.Parse(Context.User.Claims.FirstOrDefault(x => x.Type == "Id").Value);
+
+            var isOffline = await _tracker.UserDisconnected(userId, Context.ConnectionId);
+
+            if (isOffline)
+                await Clients.Others.SendAsync("UserIsOffline", userId);
+
+            await base.OnDisconnectedAsync(exception);
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs b/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs
new file mode 100644
index 0000000..f11f18d
--- /dev/null
+++ b/Tsi1.Api/Tsi1.Api/SignalR/PresenceTracker.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Tsi1.Api.SignalR
+{
+    public class PresenceTracker
+    {
+        private static readonly Dictionary<int, List<string>> OnlineUsers =
+            new Dictionary<int, List<string>>();
+
+        public Task<bool> UserConnected(int userId, string connectionId)
+        {
+            bool isOnline = false;
+            lock (OnlineUsers)
+            {
+                if (OnlineUsers.ContainsKey(userId))
+                {
+                    OnlineUsers[userId].Add(connectionId);
+                }
+                else
+                {
+                    OnlineUsers.Add(userId, new List<string> { connectionId });
+                    isOnline = true;
+                }
+            }
+
+            return Task.FromResult(isOnline);
+        }
+
+        public Task<bool> UserDisconnected(int userId, string connectionId)
+        {
+            bool isOffline = false;
+            lock (OnlineUsers)
+            {
+                if (!OnlineUsers.ContainsKey(userId)) return Task.FromResult(isOffline);
+
+                OnlineUsers[userId].Remove(connectionId);
+                if (OnlineUsers[userId].Count == 0)
+                {
+                    OnlineUsers.Remove(userId);
+                    isOffline = true;
+                }
+            }
+
+            return Task.FromResult(isOffline);
+        }
+
+        public Task<int[]> GetOnlineUsers()
+        {
+            int[] onlineUsers;
+            lock (OnlineUsers)
+            {
+                onlineUsers = OnlineUsers.OrderBy(k => k.Key).Select(k => k.Key).ToArray();
+            }
+
+            return Task.FromResult(onlineUsers);
+        }
+
+        public Task<List<string>> GetConnectionsForUser(int userId)
+        {
+            List<string> connectionIds;
+            lock (OnlineUsers)
+            {
+                connectionIds = OnlineUsers.GetValueOrDefault(userId);
+            }
+
+            return Task.FromResult(connectionIds);
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.Api/Startup.cs b/Tsi1.Api/Tsi1.Api/Startup.cs
index 9e0df62..6941e4b 100644
--- a/Tsi1.Api/Tsi1.Api/Startup.cs
+++ b/Tsi1.Api/Tsi1.Api/Startup.cs
@@ -176,6 +176,8 @@ namespace Tsi1.Api
             {
                 endpoints.MapControllers();
                 endpoints.MapHub<VideoHub>("hubs/video");
+                endpoints.MapHub<PresenceHub>("hubs/presence");
+                endpoints.MapHub<ChatHub>("hubs/chat");
             });
         }
     }
diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs
new file mode 100644
index 0000000..cf332b0
--- /dev/null
+++ b/Tsi1.Api/Tsi1.BusinessLayer/Interfaces/IChatService.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Tsi1.BusinessLayer.Helpers;
+using Tsi1.DataLayer.Entities;
+
+namespace Tsi1.BusinessLayer.Interfaces
+{
+    public interface IChatService
+    {
+        Task<Group> GetGroupByName(string groupName);
+
+        Task<ServiceResult<Group>> CreateGroup(Group group);
+
+        Task<bool> RemoveConnectionFromGroup(string connectionId);
+    }
+}
diff --git a/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs b/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs
new file mode 100644
index 0000000..b0b7653
--- /dev/null
+++ b/Tsi1.Api/Tsi1.BusinessLayer/Services/ChatService.cs
@@ -0,0 +1,61 @@
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Tsi1.BusinessLayer.Helpers;
+using Tsi1.BusinessLayer.Interfaces;
+using Tsi1.DataLayer;
+using Tsi1.DataLayer.Entities;
+
+namespace Tsi1.BusinessLayer.Services
+{
+    public class ChatService : IChatService
+    {
+        private readonly Tsi1Context _tsi1Context;
+
+        public ChatService(Tsi1Context tsi1Context)
+        {
+            _tsi1Context = tsi1Context;
+        }
+        public async Task<ServiceResult<Group>> CreateGroup(Group group)
+        {
+            var result = new ServiceResult<Group>();
+
+            _tsi1Context.Groups.Add(group);
+            try
+            {
+                await _tsi1Context.SaveChangesAsync();
+            }
+            catch (Exception)
+            {
+                result.HasError = true;
+            }
+
+            return result;
+        }
+
+        //public async Task<ServiceResult<Group>> GetGroupByName(string groupName)
+        public async Task<Group> GetGroupByName(string groupName)
+        {
+            var result = new ServiceResult<Group>();
+
+            var group = await _tsi1Context.Groups.FirstOrDefaultAsync(x => x.Name == groupName);
+
+            return group;
+        }
+
+        public async Task<bool> RemoveConnectionFromGroup(string connectionId)
+        {
+            var connection = await _tsi1Context.Connections.FirstOrDefaultAsync(x => x.ConnectionId == connectionId);
+
+            if (connection != null)
+            {
+                connection.Group = null;
+                await _tsi1Context.SaveChangesAsync();
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs b/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs
new file mode 100644
index 0000000..2e2e936
--- /dev/null
+++ b/Tsi1.Api/Tsi1.DataLayer/Entities/Connection.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Tsi1.DataLayer.Entities
+{
+    public class Connection
+    {
+        public string ConnectionId { get; set; }
+        public int UserId { get; set; }
+        public Group Group { get; set; }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs b/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs
new file mode 100644
index 0000000..38291eb
--- /dev/null
+++ b/Tsi1.Api/Tsi1.DataLayer/Entities/Group.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Tsi1.DataLayer.Entities
+{
+    public class Group
+    {
+        public Group()
+        {
+            Connections = new List<Connection>();
+        }
+
+        public string Name { get; set; }
+        public ICollection<Connection> Connections { get; set; }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs
new file mode 100644
index 0000000..7c085f9
--- /dev/null
+++ b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/ConnectionConfiguration.cs
@@ -0,0 +1,24 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Tsi1.DataLayer.Entities;
+
+namespace Tsi1.DataLayer.EntityConfiguration
+{
+    public class ConnectionConfiguration : IEntityTypeConfiguration<Connection>
+    {
+        public void Configure(EntityTypeBuilder<Connection> builder)
+        {
+            builder.HasKey(x => x.ConnectionId);
+
+            builder.Property(x => x.UserId)
+                .IsRequired();
+
+            builder.HasOne(x => x.Group)
+                .WithMany(x => x.Connections);
+
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs
new file mode 100644
index 0000000..9e64b95
--- /dev/null
+++ b/Tsi1.Api/Tsi1.DataLayer/EntityConfiguration/GroupConfiguration.cs
@@ -0,0 +1,17 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Tsi1.DataLayer.Entities;
+
+namespace Tsi1.DataLayer.EntityConfiguration
+{
+    public class GroupConfiguration : IEntityTypeConfiguration<Group>
+    {
+        public void Configure(EntityTypeBuilder<Group> builder)
+        {
+            builder.HasKey(x => x.Name);
+        }
+    }
+}
diff --git a/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs b/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs
index c9fb14f..8cfccef 100644
--- a/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs
+++ b/Tsi1.Api/Tsi1.DataLayer/Tsi1Context.cs
@@ -21,6 +21,8 @@ namespace Tsi1.DataLayer
         public DbSet<PostMessage> PostMessages { get; set; }
         public DbSet<Tenant> Tenants { get; set; }
         public DbSet<ForumUser> ForumUsers { get; set; }
+        public DbSet<Group> Groups { get; set; }
+        public DbSet<Connection> Connections { get; set; }
 
 
         public Tsi1Context(DbContextOptions options) : base(options) { }
@@ -39,6 +41,8 @@ namespace Tsi1.DataLayer
             modelBuilder.ApplyConfiguration(new PostMessageConfiguration());
             modelBuilder.ApplyConfiguration(new TenantConfiguration());
             modelBuilder.ApplyConfiguration(new ForumUserConfiguration());
+            modelBuilder.ApplyConfiguration(new GroupConfiguration());
+            modelBuilder.ApplyConfiguration(new ConnectionConfiguration());
         }
     }
 }
-- 
GitLab