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