<commit_before>using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Chat.Infrastructure;
using Chat.Models;
using Chat.ViewModels;
using Microsoft.Security.Application;
using SignalR.Hubs;
using SignalR.Samples.Hubs.Chat.ContentProviders;

namespace SignalR.Samples.Hubs.Chat
{
    public class Chat : Hub, IDisconnect
    {
        private readonly IJabbrRepository _repo;

        public Chat(IJabbrRepository repo)
        {
            _repo = repo;
        }

        private static readonly List<IContentProvider> _contentProviders = new List<IContentProvider>() {
            new ImageContentProvider(),
            new YouTubeContentProvider(),
            new CollegeHumorContentProvider(),
            new TweetContentProvider(),
            new PastieContentProvider(),
            new ImgurContentProvider()
        };

        public bool OutOfSync
        {
            get
            {
                string version = Caller.version;
                return String.IsNullOrEmpty(version) ||
                        new Version(version) != typeof(Chat).Assembly.GetName().Version;
            }
        }

        public bool Join()
        {
            Caller.version = typeof(Chat).Assembly.GetName().Version.ToString();

            // Check the user id cookie
            HttpCookie userIdCookie = Context.Cookies["userid"];
            HttpCookie userNameCookie = Context.Cookies["username"];
            HttpCookie userRoomCookie = Context.Cookies["userroom"];
            HttpCookie userHashCookie = Context.Cookies["userhash"];
            HttpCookie currentRoomCookie = Context.Cookies["currentroom"];

            // setup user 
            ChatUser user = null;

            // First try to retrieve user by id if exists
            if (userIdCookie != null && !String.IsNullOrWhiteSpace(userIdCookie.Value))
            {
                user = _repo.Users.FirstOrDefault(u => u.Id == userIdCookie.Value);
            }

            // If we couldn't get user by id try it by name server could be reset
            if (user == null && userNameCookie != null && !String.IsNullOrWhiteSpace(userNameCookie.Value))
            {
                user = AddUser(HttpUtility.UrlDecode(userNameCookie.Value));
            }

            // If we have no user return false will force user to set new nick
            if (user == null)
            {
                return false;
            }

            // If we have user hash cookie set gravatar
            if (userHashCookie != null)
            {
                SetGravatar(user, HttpUtility.UrlDecode(userHashCookie.Value));
            }

            // Update the users's client id mapping
            user.ClientId = Context.ClientId;
            user.Active = true;
            user.LastActivity = DateTime.UtcNow;
            _repo.Update();

            var userViewModel = new UserViewModel(user);

            Caller.room = null;

            if (currentRoomCookie != null)
            {
                Caller.currentRoom = HttpUtility.UrlDecode(currentRoomCookie.Value);
            }

            // Set some client state
            Caller.id = user.Id;
            Caller.name = user.Name;
            Caller.hash = user.Hash;

            // If we have room add user to room
            if (userRoomCookie != null && !String.IsNullOrWhiteSpace(userRoomCookie.Value))
            {
                foreach (var item in HttpUtility.UrlDecode(userRoomCookie.Value).Split(';'))
                {
                    var userRoom = item;
                    var room = _repo.Rooms.Where(x => x.Name == userRoom).FirstOrDefault();

                    // If user has room name in the cookie but it doesn't exists skip it!
                    if (room == null)
                    {
                        room = AddRoom(user, userRoom);
                    }

                    FixOwner(user, room);

                    // Check if the user is already in the room if so let him rejoin
                    if (IsUserInRoom(room, user))
                    {
                        HandleRejoin(room, user);
                    }
                    // if the user is not in the room join the room
                    else
                    {
                        HandleJoin(null, user, new[] { room.Name, room.Name });
                    }
                }
            }
            // if user is in a room rejoin it
            else if (IsUserInARoom(user))
            {
                // retrieve user rooms
                foreach (var room in GetUserRooms(user))
                {
                    FixOwner(user, room);
                    // handle the join of the room
                    HandleRejoin(room, user);
                }
            }
            else
            {
                // Add this user to the list of users
                Caller.addUser(userViewModel);
            }
            return true;
        }

        private static void FixOwner(ChatUser user, ChatRoom room)
        {
            if (room.Owner != null &&
                room.Owner.Name.Equals(user.Name, StringComparison.OrdinalIgnoreCase))
            {
                room.Owner = user;
            }
        }

        public void Send(string content)
        {
            if (OutOfSync)
            {
                throw new InvalidOperationException("Chat was just updated, please refresh you browser");
            }

            content = Sanitizer.GetSafeHtmlFragment(content);

            // See if this is a valid command (starts with /)
            if (TryHandleCommand(content))
            {
                return;
            }

            string roomName = Caller.room;
            string name = Caller.name;

            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();

            ChatUser user = tuple.Item1;
            ChatRoom chatRoom = tuple.Item2;

            // Update activity *after* ensuring the user, this forces them to be active
            UpdateActivity();

            HashSet<string> links;
            var messageText = Transform(content, out links);

            var chatMessage = new ChatMessage
            {
                Id = Guid.NewGuid().ToString("d"),
                User = user,
                Content = messageText,
                When = DateTimeOffset.UtcNow
            };

            chatRoom.Messages.Add(chatMessage);
            chatRoom.LastActivity = DateTime.UtcNow;
            _repo.Update();

            var messageViewModel = new MessageViewModel(chatMessage, chatRoom);

            Clients[chatRoom.Name].addMessage(messageViewModel);

            if (!links.Any())
            {
                return;
            }

            ProcessUrls(links, chatRoom, chatMessage);
        }

        public void Disconnect()
        {
            Disconnect(Context.ClientId);
        }

        public IEnumerable<RoomViewModel> GetRooms()
        {
            var rooms = _repo.Rooms.Select(r => new RoomViewModel
            {
                Name = r.Name,
                Count = r.Users.Count
            });

            return rooms;
        }

        public IEnumerable<UserViewModel> GetUsers(string room)
        {
            if (String.IsNullOrEmpty(room))
            {
                return Enumerable.Empty<UserViewModel>();
            }

            ChatRoom chatRoom = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(room, StringComparison.OrdinalIgnoreCase));

            if (chatRoom == null)
            {
                return Enumerable.Empty<UserViewModel>();
            }

            return chatRoom.Users.Select(u => new UserViewModel(u, chatRoom));
        }

        public IEnumerable<MessageViewModel> GetRecentMessages(string room)
        {
            if (String.IsNullOrEmpty(room))
            {
                return Enumerable.Empty<MessageViewModel>();
            }

            ChatRoom chatRoom = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(room, StringComparison.OrdinalIgnoreCase));

            if (chatRoom == null)
            {
                return Enumerable.Empty<MessageViewModel>();
            }

            return (from m in chatRoom.Messages
                    orderby m.When descending
                    select new MessageViewModel(m, chatRoom)).Take(20).Reverse();
        }

        public ChatRoom[] GetUserRooms(ChatUser user)
        {
            return user.Rooms.ToArray();
        }

        public void Typing(bool isTyping)
        {
            if (OutOfSync)
            {
                return;
            }

            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();

            ChatUser user = tuple.Item1;
            ChatRoom chatRoom = tuple.Item2;
            var userViewModel = new UserViewModel(user, chatRoom);

            if (isTyping)
            {
                UpdateActivity();
            }

            if (chatRoom != null)
            {
                Clients[chatRoom.Name].setTyping(userViewModel, isTyping);
            }
            else
            {
                Caller.setTyping(userViewModel, isTyping);
            }
        }

        private void Disconnect(string clientId)
        {
            ChatUser user = _repo.Users.FirstOrDefault(u => u.ClientId == clientId);

            if (user == null)
            {
                return;
            }

            LeaveAllRooms(user);

            // Remove the user
            _repo.Remove(user);
        }

        private void UpdateActivity()
        {
            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();
            ChatUser user = tuple.Item1;
            ChatRoom room = tuple.Item2;

            if (user == null || room == null)
            {
                return;
            }

            Clients[room.Name].updateActivity(new UserViewModel(user, room));
        }

        private void ProcessUrls(IEnumerable<string> links, ChatRoom chatRoom, ChatMessage chatMessage)
        {
            // REVIEW: is this safe to do? We're holding on to this instance 
            // when this should really be a fire and forget.
            var contentTasks = links.Select(ExtractContent).ToArray();
            Task.Factory.ContinueWhenAll(contentTasks, tasks =>
            {
                foreach (var task in tasks)
                {
                    if (task.IsFaulted)
                    {
                        Trace.TraceError(task.Exception.GetBaseException().Message);
                        continue;
                    }

                    if (String.IsNullOrEmpty(task.Result))
                    {
                        continue;
                    }

                    // Try to get content from each url we're resolved in the query
                    string extractedContent = "<p>" + task.Result + "</p>";

                    // If we did get something, update the message and notify all clients
                    chatMessage.Content += extractedContent;
                    _repo.Update();

                    Clients[chatRoom.Name].addMessageContent(chatMessage.Id, extractedContent);
                }
            });
        }

        private bool TryHandleCommand(string command)
        {
            command = command.Trim();
            if (!command.StartsWith("/"))
            {
                return false;
            }

            string room = Caller.room;
            string name = Caller.name;
            string[] parts = command.Substring(1).Split(' ');
            string commandName = parts[0];

            if (commandName.Equals("help", StringComparison.OrdinalIgnoreCase))
            {
                HandleHelp();

                return true;
            }
            else if (commandName.Equals("nick", StringComparison.OrdinalIgnoreCase))
            {
                HandleNick(name, parts);

                return true;
            }
            else
            {
                ChatUser user = EnsureUser();
                if (commandName.Equals("rooms", StringComparison.OrdinalIgnoreCase))
                {
                    HandleRooms();

                    return true;
                }
                else if (commandName.Equals("list", StringComparison.OrdinalIgnoreCase))
                {
                    HandleList(parts);
                    return true;
                }
                else if (commandName.Equals("who", StringComparison.OrdinalIgnoreCase))
                {
                    HandleWho(parts);
                    return true;
                }
                else if (commandName.Equals("join", StringComparison.OrdinalIgnoreCase))
                {
                    HandleJoin(room, user, parts);

                    return true;
                }
                else if (commandName.Equals("msg", StringComparison.OrdinalIgnoreCase))
                {
                    HandleMsg(user, parts);

                    return true;
                }
                else if (commandName.Equals("gravatar", StringComparison.OrdinalIgnoreCase))
                {
                    HandleGravatar(user, parts);

                    return true;
                }
                else if (commandName.Equals("leave", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
                {
                    HandleLeave(user, parts);

                    return true;
                }
                else if (commandName.Equals("nudge", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
                {
                    HandleNudge(user, parts);

                    return true;
                }
                else
                {
                    Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();
                    if (commandName.Equals("me", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleMe(tuple.Item2, tuple.Item1, parts);
                        return true;
                    }
                    else if (commandName.Equals("leave", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleLeave(tuple.Item2, tuple.Item1);

                        return true;
                    }
                    else if (commandName.Equals("nudge", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleNudge(tuple.Item2, tuple.Item1, parts);

                        return true;
                    }
                    else if (commandName.Equals("kick", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleKick(tuple.Item1, tuple.Item2, parts);

                        return true;
                    }

                    throw new InvalidOperationException(String.Format("'{0}' is not a valid command.", parts[0]));
                }
            }
        }

        private void HandleKick(ChatUser chatUser, ChatRoom chatRoom, string[] parts)
        {
            if (chatRoom.Owner != chatUser)
            {
                throw new InvalidOperationException("You are not the owner of " + chatRoom.Name);
            }

            if (parts.Length == 1)
            {
                throw new InvalidOperationException("Who are you trying to kick?");
            }

            if (chatRoom.Users.Count == 1)
            {
                throw new InvalidOperationException("You're the only person in here...");
            }

            string targetUserName = parts[1];
            var targetUser = _repo.Users.FirstOrDefault(s => s.Name.Equals(targetUserName, StringComparison.OrdinalIgnoreCase));

            if (targetUser == null)
            {
                throw new InvalidOperationException(String.Format("Couldn't find any user named '{0}'.", targetUserName));
            }

            if (targetUser == chatUser)
            {
                throw new InvalidOperationException("Why would you want to kick yourself?");
            }

            // Kick the user
            HandleLeave(chatRoom, targetUser);

            // Tell the user that they were kicked
            Clients[targetUser.ClientId].kick(chatRoom.Name);
        }

        private void HandleWho(string[] parts)
        {
            if (parts.Length == 1)
            {
                Caller.listUsers(_repo.Users.Select(s => s.Name));
                return;
            }

            var name = NormalizeUserName(parts[1]);

            var exactUserMatch = _repo.Users.FirstOrDefault(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

            if (exactUserMatch != null)
            {
                Caller.showUsersRoomList(exactUserMatch.Name, exactUserMatch.Rooms.Select(r => r.Name));
                return;
            }

            var users = _repo.Users.Where(s => s.Name.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);

            if (users.Count() == 1)
            {
                var user = users.First();
                Caller.showUsersRoomList(user.Name, user.Rooms.Select(r => r.Name));
            }
            else
            {
                Caller.listUsers(users.Select(s => s.Name));
            }
        }

        private void HandleList(string[] parts)
        {
            if (parts.Length < 2)
            {
                throw new InvalidOperationException("List users in which room?");
            }

            if (String.IsNullOrWhiteSpace(parts[1]))
            {
                throw new InvalidOperationException("Room name cannot be blank!");
            }

            var room = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(parts[1], StringComparison.OrdinalIgnoreCase));

            if (room == null)
            {
                throw new InvalidOperationException("No room with that name!");
            }

            var names = room.Users.Select(s => s.Name);
            Caller.showUsersInRoom(room.Name, names);
        }

        private void HandleHelp()
        {
            Caller.showCommands(new[] { 
                new { Name = "help", Description = "Shows the list of commands" },
                new { Name = "nick", Description = "/nick changes your nickname" },
                new { Name = "join", Description = "Type /join room -- to join a channel of your choice" },
                new { Name = "me", Description = "Type /me 'does anything'" },
                new { Name = "msg", Description = "Type /msg @nickname (message) to send a private message to nickname. @ is optional." },
                new { Name = "leave", Description = "Type /leave to leave the current room. Type /leave [room name] to leave a specific room." },
                new { Name = "rooms", Description = "Type /rooms to show the list of rooms" },
                new { Name = "who", Description = "Type /who to show a list of all users, /who [name] to the rooms that user is in" },
                new { Name = "list", Description = "Type /list (room) to show a list of users in the room" },
                new { Name = "gravatar", Description = "Type \"/gravatar email\" to set your gravatar." },
                new { Name = "nudge", Description = "Type \"/nudge\" to send a nudge to the whole room, or \"/nudge @nickname\" to nudge a particular user. @ is optional." }
            });
        }

        private void HandleLeave(ChatUser user, string[] parts)
        {
            if (String.IsNullOrWhiteSpace(parts[1]))
            {
                throw new InvalidOperationException("Room name cannot be blank!");
            }

            var room = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(parts[1], StringComparison.OrdinalIgnoreCase));

            if (room == null)
            {
                throw new InvalidOperationException("No room with that name!");
            }

            HandleLeave(room, user);
        }

        private void HandleLeave(ChatRoom room, ChatUser user)
        {
            room.Users.Remove(user);<commit_msg>Fix issue with leaving room when room name specified.<commit_after>using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Chat.Infrastructure;
using Chat.Models;
using Chat.ViewModels;
using Microsoft.Security.Application;
using SignalR.Hubs;
using SignalR.Samples.Hubs.Chat.ContentProviders;

namespace SignalR.Samples.Hubs.Chat
{
    public class Chat : Hub, IDisconnect
    {
        private readonly IJabbrRepository _repo;

        public Chat(IJabbrRepository repo)
        {
            _repo = repo;
        }

        private static readonly List<IContentProvider> _contentProviders = new List<IContentProvider>() {
            new ImageContentProvider(),
            new YouTubeContentProvider(),
            new CollegeHumorContentProvider(),
            new TweetContentProvider(),
            new PastieContentProvider(),
            new ImgurContentProvider()
        };

        public bool OutOfSync
        {
            get
            {
                string version = Caller.version;
                return String.IsNullOrEmpty(version) ||
                        new Version(version) != typeof(Chat).Assembly.GetName().Version;
            }
        }

        public bool Join()
        {
            Caller.version = typeof(Chat).Assembly.GetName().Version.ToString();

            // Check the user id cookie
            HttpCookie userIdCookie = Context.Cookies["userid"];
            HttpCookie userNameCookie = Context.Cookies["username"];
            HttpCookie userRoomCookie = Context.Cookies["userroom"];
            HttpCookie userHashCookie = Context.Cookies["userhash"];
            HttpCookie currentRoomCookie = Context.Cookies["currentroom"];

            // setup user 
            ChatUser user = null;

            // First try to retrieve user by id if exists
            if (userIdCookie != null && !String.IsNullOrWhiteSpace(userIdCookie.Value))
            {
                user = _repo.Users.FirstOrDefault(u => u.Id == userIdCookie.Value);
            }

            // If we couldn't get user by id try it by name server could be reset
            if (user == null && userNameCookie != null && !String.IsNullOrWhiteSpace(userNameCookie.Value))
            {
                user = AddUser(HttpUtility.UrlDecode(userNameCookie.Value));
            }

            // If we have no user return false will force user to set new nick
            if (user == null)
            {
                return false;
            }

            // If we have user hash cookie set gravatar
            if (userHashCookie != null)
            {
                SetGravatar(user, HttpUtility.UrlDecode(userHashCookie.Value));
            }

            // Update the users's client id mapping
            user.ClientId = Context.ClientId;
            user.Active = true;
            user.LastActivity = DateTime.UtcNow;
            _repo.Update();

            var userViewModel = new UserViewModel(user);

            Caller.room = null;

            if (currentRoomCookie != null)
            {
                Caller.currentRoom = HttpUtility.UrlDecode(currentRoomCookie.Value);
            }

            // Set some client state
            Caller.id = user.Id;
            Caller.name = user.Name;
            Caller.hash = user.Hash;

            // If we have room add user to room
            if (userRoomCookie != null && !String.IsNullOrWhiteSpace(userRoomCookie.Value))
            {
                foreach (var item in HttpUtility.UrlDecode(userRoomCookie.Value).Split(';'))
                {
                    var userRoom = item;
                    var room = _repo.Rooms.Where(x => x.Name == userRoom).FirstOrDefault();

                    // If user has room name in the cookie but it doesn't exists skip it!
                    if (room == null)
                    {
                        room = AddRoom(user, userRoom);
                    }

                    FixOwner(user, room);

                    // Check if the user is already in the room if so let him rejoin
                    if (IsUserInRoom(room, user))
                    {
                        HandleRejoin(room, user);
                    }
                    // if the user is not in the room join the room
                    else
                    {
                        HandleJoin(null, user, new[] { room.Name, room.Name });
                    }
                }
            }
            // if user is in a room rejoin it
            else if (IsUserInARoom(user))
            {
                // retrieve user rooms
                foreach (var room in GetUserRooms(user))
                {
                    FixOwner(user, room);
                    // handle the join of the room
                    HandleRejoin(room, user);
                }
            }
            else
            {
                // Add this user to the list of users
                Caller.addUser(userViewModel);
            }
            return true;
        }

        private static void FixOwner(ChatUser user, ChatRoom room)
        {
            if (room.Owner != null &&
                room.Owner.Name.Equals(user.Name, StringComparison.OrdinalIgnoreCase))
            {
                room.Owner = user;
            }
        }

        public void Send(string content)
        {
            if (OutOfSync)
            {
                throw new InvalidOperationException("Chat was just updated, please refresh you browser");
            }

            content = Sanitizer.GetSafeHtmlFragment(content);

            // See if this is a valid command (starts with /)
            if (TryHandleCommand(content))
            {
                return;
            }

            string roomName = Caller.room;
            string name = Caller.name;

            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();

            ChatUser user = tuple.Item1;
            ChatRoom chatRoom = tuple.Item2;

            // Update activity *after* ensuring the user, this forces them to be active
            UpdateActivity();

            HashSet<string> links;
            var messageText = Transform(content, out links);

            var chatMessage = new ChatMessage
            {
                Id = Guid.NewGuid().ToString("d"),
                User = user,
                Content = messageText,
                When = DateTimeOffset.UtcNow
            };

            chatRoom.Messages.Add(chatMessage);
            chatRoom.LastActivity = DateTime.UtcNow;
            _repo.Update();

            var messageViewModel = new MessageViewModel(chatMessage, chatRoom);

            Clients[chatRoom.Name].addMessage(messageViewModel);

            if (!links.Any())
            {
                return;
            }

            ProcessUrls(links, chatRoom, chatMessage);
        }

        public void Disconnect()
        {
            Disconnect(Context.ClientId);
        }

        public IEnumerable<RoomViewModel> GetRooms()
        {
            var rooms = _repo.Rooms.Select(r => new RoomViewModel
            {
                Name = r.Name,
                Count = r.Users.Count
            });

            return rooms;
        }

        public IEnumerable<UserViewModel> GetUsers(string room)
        {
            if (String.IsNullOrEmpty(room))
            {
                return Enumerable.Empty<UserViewModel>();
            }

            ChatRoom chatRoom = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(room, StringComparison.OrdinalIgnoreCase));

            if (chatRoom == null)
            {
                return Enumerable.Empty<UserViewModel>();
            }

            return chatRoom.Users.Select(u => new UserViewModel(u, chatRoom));
        }

        public IEnumerable<MessageViewModel> GetRecentMessages(string room)
        {
            if (String.IsNullOrEmpty(room))
            {
                return Enumerable.Empty<MessageViewModel>();
            }

            ChatRoom chatRoom = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(room, StringComparison.OrdinalIgnoreCase));

            if (chatRoom == null)
            {
                return Enumerable.Empty<MessageViewModel>();
            }

            return (from m in chatRoom.Messages
                    orderby m.When descending
                    select new MessageViewModel(m, chatRoom)).Take(20).Reverse();
        }

        public ChatRoom[] GetUserRooms(ChatUser user)
        {
            return user.Rooms.ToArray();
        }

        public void Typing(bool isTyping)
        {
            if (OutOfSync)
            {
                return;
            }

            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();

            ChatUser user = tuple.Item1;
            ChatRoom chatRoom = tuple.Item2;
            var userViewModel = new UserViewModel(user, chatRoom);

            if (isTyping)
            {
                UpdateActivity();
            }

            if (chatRoom != null)
            {
                Clients[chatRoom.Name].setTyping(userViewModel, isTyping);
            }
            else
            {
                Caller.setTyping(userViewModel, isTyping);
            }
        }

        private void Disconnect(string clientId)
        {
            ChatUser user = _repo.Users.FirstOrDefault(u => u.ClientId == clientId);

            if (user == null)
            {
                return;
            }

            LeaveAllRooms(user);

            // Remove the user
            _repo.Remove(user);
        }

        private void UpdateActivity()
        {
            Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();
            ChatUser user = tuple.Item1;
            ChatRoom room = tuple.Item2;

            if (user == null || room == null)
            {
                return;
            }

            Clients[room.Name].updateActivity(new UserViewModel(user, room));
        }

        private void ProcessUrls(IEnumerable<string> links, ChatRoom chatRoom, ChatMessage chatMessage)
        {
            // REVIEW: is this safe to do? We're holding on to this instance 
            // when this should really be a fire and forget.
            var contentTasks = links.Select(ExtractContent).ToArray();
            Task.Factory.ContinueWhenAll(contentTasks, tasks =>
            {
                foreach (var task in tasks)
                {
                    if (task.IsFaulted)
                    {
                        Trace.TraceError(task.Exception.GetBaseException().Message);
                        continue;
                    }

                    if (String.IsNullOrEmpty(task.Result))
                    {
                        continue;
                    }

                    // Try to get content from each url we're resolved in the query
                    string extractedContent = "<p>" + task.Result + "</p>";

                    // If we did get something, update the message and notify all clients
                    chatMessage.Content += extractedContent;
                    _repo.Update();

                    Clients[chatRoom.Name].addMessageContent(chatMessage.Id, extractedContent);
                }
            });
        }

        private bool TryHandleCommand(string command)
        {
            command = command.Trim();
            if (!command.StartsWith("/"))
            {
                return false;
            }

            string room = Caller.room;
            string name = Caller.name;
            string[] parts = command.Substring(1).Split(' ');
            string commandName = parts[0];

            if (commandName.Equals("help", StringComparison.OrdinalIgnoreCase))
            {
                HandleHelp();

                return true;
            }
            else if (commandName.Equals("nick", StringComparison.OrdinalIgnoreCase))
            {
                HandleNick(name, parts);

                return true;
            }
            else
            {
                ChatUser user = EnsureUser();
                if (commandName.Equals("rooms", StringComparison.OrdinalIgnoreCase))
                {
                    HandleRooms();

                    return true;
                }
                else if (commandName.Equals("list", StringComparison.OrdinalIgnoreCase))
                {
                    HandleList(parts);
                    return true;
                }
                else if (commandName.Equals("who", StringComparison.OrdinalIgnoreCase))
                {
                    HandleWho(parts);
                    return true;
                }
                else if (commandName.Equals("join", StringComparison.OrdinalIgnoreCase))
                {
                    HandleJoin(room, user, parts);

                    return true;
                }
                else if (commandName.Equals("msg", StringComparison.OrdinalIgnoreCase))
                {
                    HandleMsg(user, parts);

                    return true;
                }
                else if (commandName.Equals("gravatar", StringComparison.OrdinalIgnoreCase))
                {
                    HandleGravatar(user, parts);

                    return true;
                }
                else if (commandName.Equals("leave", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
                {
                    HandleLeave(user, parts);

                    return true;
                }
                else if (commandName.Equals("nudge", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
                {
                    HandleNudge(user, parts);

                    return true;
                }
                else
                {
                    Tuple<ChatUser, ChatRoom> tuple = EnsureUserAndRoom();
                    if (commandName.Equals("me", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleMe(tuple.Item2, tuple.Item1, parts);
                        return true;
                    }
                    else if (commandName.Equals("leave", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleLeave(tuple.Item2, tuple.Item1);

                        return true;
                    }
                    else if (commandName.Equals("nudge", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleNudge(tuple.Item2, tuple.Item1, parts);

                        return true;
                    }
                    else if (commandName.Equals("kick", StringComparison.OrdinalIgnoreCase))
                    {
                        HandleKick(tuple.Item1, tuple.Item2, parts);

                        return true;
                    }

                    throw new InvalidOperationException(String.Format("'{0}' is not a valid command.", parts[0]));
                }
            }
        }

        private void HandleKick(ChatUser chatUser, ChatRoom chatRoom, string[] parts)
        {
            if (chatRoom.Owner != chatUser)
            {
                throw new InvalidOperationException("You are not the owner of " + chatRoom.Name);
            }

            if (parts.Length == 1)
            {
                throw new InvalidOperationException("Who are you trying to kick?");
            }

            if (chatRoom.Users.Count == 1)
            {
                throw new InvalidOperationException("You're the only person in here...");
            }

            string targetUserName = parts[1];
            var targetUser = _repo.Users.FirstOrDefault(s => s.Name.Equals(targetUserName, StringComparison.OrdinalIgnoreCase));

            if (targetUser == null)
            {
                throw new InvalidOperationException(String.Format("Couldn't find any user named '{0}'.", targetUserName));
            }

            if (targetUser == chatUser)
            {
                throw new InvalidOperationException("Why would you want to kick yourself?");
            }

            // Kick the user
            HandleLeave(chatRoom, targetUser);

            // Tell the user that they were kicked
            Clients[targetUser.ClientId].kick(chatRoom.Name);
        }

        private void HandleWho(string[] parts)
        {
            if (parts.Length == 1)
            {
                Caller.listUsers(_repo.Users.Select(s => s.Name));
                return;
            }

            var name = NormalizeUserName(parts[1]);

            var exactUserMatch = _repo.Users.FirstOrDefault(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

            if (exactUserMatch != null)
            {
                Caller.showUsersRoomList(exactUserMatch.Name, exactUserMatch.Rooms.Select(r => r.Name));
                return;
            }

            var users = _repo.Users.Where(s => s.Name.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);

            if (users.Count() == 1)
            {
                var user = users.First();
                Caller.showUsersRoomList(user.Name, user.Rooms.Select(r => r.Name));
            }
            else
            {
                Caller.listUsers(users.Select(s => s.Name));
            }
        }

        private void HandleList(string[] parts)
        {
            if (parts.Length < 2)
            {
                throw new InvalidOperationException("List users in which room?");
            }

            if (String.IsNullOrWhiteSpace(parts[1]))
            {
                throw new InvalidOperationException("Room name cannot be blank!");
            }

            var room = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(parts[1], StringComparison.OrdinalIgnoreCase));

            if (room == null)
            {
                throw new InvalidOperationException("No room with that name!");
            }

            var names = room.Users.Select(s => s.Name);
            Caller.showUsersInRoom(room.Name, names);
        }

        private void HandleHelp()
        {
            Caller.showCommands(new[] { 
                new { Name = "help", Description = "Shows the list of commands" },
                new { Name = "nick", Description = "/nick changes your nickname" },
                new { Name = "join", Description = "Type /join room -- to join a channel of your choice" },
                new { Name = "me", Description = "Type /me 'does anything'" },
                new { Name = "msg", Description = "Type /msg @nickname (message) to send a private message to nickname. @ is optional." },
                new { Name = "leave", Description = "Type /leave to leave the current room. Type /leave [room name] to leave a specific room." },
                new { Name = "rooms", Description = "Type /rooms to show the list of rooms" },
                new { Name = "who", Description = "Type /who to show a list of all users, /who [name] to the rooms that user is in" },
                new { Name = "list", Description = "Type /list (room) to show a list of users in the room" },
                new { Name = "gravatar", Description = "Type \"/gravatar email\" to set your gravatar." },
                new { Name = "nudge", Description = "Type \"/nudge\" to send a nudge to the whole room, or \"/nudge @nickname\" to nudge a particular user. @ is optional." }
            });
        }

        private void HandleLeave(ChatUser user, string[] parts)
        {
            string roomName = parts[1];
            if (String.IsNullOrWhiteSpace(roomName))
            {
                throw new InvalidOperationException("Room name cannot be blank!");
            }

            var room = _repo.Rooms.FirstOrDefault(r => r.Name.Equals(roomName, StringComparison.OrdinalIgnoreCase));

            if (room == null)
            {
                throw new InvalidOperationException("No room with that name!");
            }

            HandleLeave(room, user);
        }

        private void HandleLeave(ChatRoom room, ChatUser user)
        {
            room.Users.Remove(user);
{'fraction_non_alphanumeric': 0.09008227597145123, 'fraction_numerical': 0.0017099524187153053, 'mean_word_length': 1.3620346523062514, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 73, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1588342', 'n_tokens_mistral': 10015, 'n_tokens_neox': 9354, 'n_words': 2426}