// // MyHub.cs // // Author: // Paul Schneider // // Copyright (c) 2016 GNU GPL // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this program. If not, see . using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hosting; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; namespace Yavsc { using System; using System.Collections.Concurrent; using Microsoft.AspNet.WebUtilities; using Microsoft.Data.Entity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Models; using Models.Chat; public class ChatHub : Hub, IDisposable { ApplicationDbContext _dbContext; ILogger _logger; public ChatHub() { var scope = Startup.Services.GetRequiredService().CreateScope(); _dbContext = scope.ServiceProvider.GetService(); var loggerFactory = scope.ServiceProvider.GetService(); _logger = loggerFactory.CreateLogger(); } public override async Task OnConnected() { bool isAuth = false; string userName = setUserName(); if (Context.User != null) { isAuth = Context.User.Identity.IsAuthenticated; var group = isAuth ? Constants.HubGroupAuthenticated : Constants.HubGroupAnonymous; // Log ("Cx: " + group); await Groups.Add(Context.ConnectionId, group); if (isAuth) { _logger.LogInformation("Authenticated chat user"); var userId = _dbContext.Users.First(u=>u.UserName == userName).Id; var userHadConnections = _dbContext.ChatConnection.Any(accx => accx.ConnectionId == Context.ConnectionId); if (userHadConnections) { var ccx = _dbContext.ChatConnection.First(c=> c.ConnectionId == Context.ConnectionId); ccx.Connected=true; } else _dbContext.ChatConnection.Add(new ChatConnection { ApplicationUserId = userId, ConnectionId = Context.ConnectionId, UserAgent = Context.Request.Headers["User-Agent"], Connected = true }); _dbContext.SaveChanges(); // TODO ChatHubConnectioinFlags } else { // FIXME is this line reached ? _logger.LogInformation("Anonymous chat user (first use case)"); throw new NotSupportedException(); } } else { // TODO var uname = Context.Request.QueryString[Constants.KeyParamChatUserName] ?? "anon"; await Groups.Add(Context.ConnectionId, Constants.HubGroupAnonymous); } // TODO only notify followers Clients.Group(Constants.HubGroupAuthenticated).notify(NotificationTypes.Connected, Context.ConnectionId, userName); await base.OnConnected(); } static ConcurrentDictionary ChatUserNames = new ConcurrentDictionary(); string setUserName() { if (Context.User!=null) if (Context.User.Identity.IsAuthenticated) { ChatUserNames[Context.ConnectionId]=Context.User.Identity.Name; _logger.LogInformation($"chat user name set to : {Context.User.Identity.Name}"); return Context.User.Identity.Name; } anonymousSequence++; var aname = $"{Constants.AnonymousUserNamePrefix}{anonymousSequence}"; ChatUserNames[Context.ConnectionId]=aname; _logger.LogInformation($"Anonymous chat user name set to : {aname}"); return aname; } static long anonymousSequence=0; public override Task OnDisconnected(bool stopCalled) { string userName = Context.User?.Identity.Name; Clients.Group("authenticated").notify(NotificationTypes.DisConnected, Context.ConnectionId, userName); if (userName != null) { var cx = _dbContext.ChatConnection.SingleOrDefault(c => c.ConnectionId == Context.ConnectionId); if (cx != null) { if (stopCalled) { var user = _dbContext.Users.Single(u => u.UserName == userName); user.Connections.Remove(cx); } else { cx.Connected = false; } _dbContext.SaveChanges(); } } return base.OnDisconnected(stopCalled); } public override Task OnReconnected() { if (Context.User != null) if (Context.User.Identity.IsAuthenticated) { var userName = Context.User.Identity.Name; var user = _dbContext.Users.FirstOrDefault(u=>u.UserName == userName); if (user == null) _logger.LogWarning($"null user with <{userName}> & Context.User.Identity.IsAuthenticated"); var userId = user.Id; var userHadConnections = _dbContext.ChatConnection.Any(accx => accx.ConnectionId == Context.ConnectionId); if (userHadConnections) { var ccx = _dbContext.ChatConnection.First(c=> c.ConnectionId == Context.ConnectionId); ccx.Connected=true; } else _dbContext.ChatConnection.Add(new ChatConnection { ApplicationUserId = userId, ConnectionId = Context.ConnectionId, UserAgent = Context.Request.Headers["User-Agent"], Connected = true }); _dbContext.SaveChanges(); Clients.Group("authenticated").notify(NotificationTypes.Reconnected, Context.ConnectionId, userName); } return base.OnReconnected(); } static ConcurrentDictionary Channels = new ConcurrentDictionary(); public class ChatRoomInfo { public string Name ; public Dictionary Users = new Dictionary(); public string Topic; } public void Join(string roomName) { _logger.LogInformation("a client for "+roomName); var userName = ChatUserNames[Context.ConnectionId]; _logger.LogInformation($" chat user : {userName}"); var roomGroupName = "room_"+roomName; ChatRoomInfo chanInfo; if (Channels.ContainsKey(roomName)) { if (Channels.TryGetValue(roomName, out chanInfo)) { _logger.LogInformation("room is avaible."); if (chanInfo.Users.ContainsKey(Context.ConnectionId)) _logger.LogWarning("user already joined."); else { chanInfo.Users.Add(Context.ConnectionId, userName); Groups.Add(Context.ConnectionId,roomGroupName); } Clients.Caller.onJoined(chanInfo); Clients.Group("room_"+roomName).notify( NotificationTypes.UserJoin, Context.ConnectionId, Clients.Caller.UserName); _logger.LogInformation("exiting ok."); return; } else { _logger.LogInformation("room seemd to be avaible ... but we could get no info on it."); Clients.Caller.notify(NotificationTypes.Error, "join get chan failed ..."); return; } } // chan was almost empty _logger.LogInformation("joining empty chan."); var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName); chanInfo = new ChatRoomInfo(); chanInfo.Users.Add(Context.ConnectionId, userName); if (room!=null) { _logger.LogInformation("existent room."); chanInfo.Topic = room.Topic; chanInfo.Name = room.Name; } else { // a first join, we create it. _logger.LogInformation("room creation."); chanInfo.Name = roomName; chanInfo.Topic = ""; } if (Channels.TryAdd(roomName, chanInfo)) { Groups.Add(Context.ConnectionId, roomGroupName); Clients.Caller.onJoined(chanInfo); } else _logger.LogError("Chan create failed unexpectly..."); } [Authorize] public void Register (string room ) { var existent = _dbContext.ChatRoom.Any(r => r.Name == room); if (existent) { Clients.Caller.notify(NotificationTypes.Error, "already registered."); return; } string userName = Context.User.Identity.Name; var user = _dbContext.Users.FirstOrDefault(u=>u.UserName == userName); var newroom = new ChatRoom { Name = room, OwnerId = user.Id }; ChatRoomInfo chanInfo; if (Channels.TryGetValue(room, out chanInfo)) { // TODO get and require some admin status for current user on this chan newroom.Topic = chanInfo.Topic; } newroom.LatestJoinPart = DateTime.Now; _dbContext.ChatRoom.Add(newroom); _dbContext.SaveChanges(user.Id); } /** TODO chan register on server command room = new ChatRoom { Name = roomName, OwnerId = uid }; _dbContext.ChatRoom.Add(room); _dbContext.SaveChanges(uid); room.LatestJoinPart = DateTime.Now; chanInfo.Topic = room.Topic; */ public void Part (string roomName, string reason) { ChatRoomInfo chanInfo; if (Channels.TryGetValue(roomName, out chanInfo)) { var roomGroupName = "room_"+roomName; Groups.Remove(Context.ConnectionId, roomGroupName); var group = Clients.Group(roomGroupName); var username = ChatUserNames[Context.ConnectionId]; group.notify( NotificationTypes.UserPart, Context.ConnectionId, new { username, reason } ); chanInfo.Users.Remove(Context.ConnectionId); ChatRoomInfo deadchanInfo; if (chanInfo.Users.Count==0) if (Channels.TryRemove(roomName, out deadchanInfo)) { var room = _dbContext.ChatRoom.FirstOrDefault(r => r.Name == roomName); room.LatestJoinPart = DateTime.Now; _dbContext.SaveChanges(); } } else { Clients.Caller.notify(NotificationTypes.Error, "not joint"); } } public void Send(string roomName, string message) { var groupname = "room_"+roomName; ChatRoomInfo chanInfo; if (Channels.TryGetValue(roomName, out chanInfo)) { if (!chanInfo.Users.ContainsKey(Context.ConnectionId)){ Clients.Caller.notify(NotificationTypes.Error, $"could not join channel ({roomName})"); return; } string uname = ChatUserNames[Context.ConnectionId]; Clients.Group(groupname).addMessage(uname, roomName, message); } else { Clients.Caller.notify(NotificationTypes.Error, $"could not join channel ({roomName})"); return; } } [Authorize] public void SendPV(string connectionId, string message) { if (!Context.User.IsInRole(Constants.AdminGroupName)) { var bl = _dbContext.BlackListed .Include(r => r.User) .Where(r=>r.User.UserName == Context.User.Identity.Name) .Select(r=>r.OwnerId); if (bl!=null) foreach (string uid in bl) { if (_dbContext.ChatConnection.Any(cx => cx.ApplicationUserId==uid && cx.Connected)) Clients.Caller.notify(NotificationTypes.PrivateMessageDenied, connectionId); return ; } } var cli = Clients.Client(connectionId); cli.addPV(Context.User.Identity.Name, message); } [Authorize] public void SendStream(string connectionId, long streamId, string message) { var sender = Context.User.Identity.Name; var cli = Clients.Client(connectionId); cli.addStreamInfo(sender, streamId, message); } public void Abort() { var cx = _dbContext.ChatConnection.SingleOrDefault(c=>c.ConnectionId == Context.ConnectionId); if (cx!=null) { _dbContext.ChatConnection.Remove(cx); _dbContext.SaveChanges(); } } } }