about summary refs log blame commit diff
path: root/IRCStates/Server.cs
blob: 2cbbb92a8b9c97c29908f181afc13c3f60bf508a (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                
                                 
                           
                  
                
 
                   


                       
                                                                  

                                                  
                                                              







                                                  



                                                              
                                                             




























                                                                     




                                                                             



                                                               
 




                                                
                                          



                                                       




                                                 
                                             



                                                         





                                                          
         











                                                                        
                        

         





                                                                              

                                  
                                                           


                        




                                                 
                                             




                                                                                                 




                                                
                                           
         
                                                                           

         




                                               
                                               
         
                                                                      

         





                                                                           







                                                                




                                                                                              






                                                                        












                                                                      








                                                                                                      
                                                      









                                                                      
                                   












                                                                                                   





                                                                              













                                                                                                                  
                                                                                     




























                                                                                            






                                                                      
                                                                         


                                          
                                                    

                                                                       
                                                    

         




                                                                



                                          








































                                                                       
 
                        

         




                                       

                                             



                                                                 
                                    












                                                 

         




                                       

                                          



                                                                 
                                    












                                                 

         




                                       

                                             



                                                                 
                                    












                                                 

         




                                       

                                         













                                                                                       
                                                                                                     



                               
                                                 

                                   

                                                            



                           
                                                     















                                                                                                 
                                                                                             







                                                

         




                                        

                                              


                                                       

         




                                       

                                             




                                                                 
                                    














                                                 

         




                                         

                                               























                                                     

         




                                         

                                          





























                                                                             

         




                                        

                                              























                                                              

         




                                           

                                                 












                                                     

         




                                                        

                                             



                                                                                           

                                                        




                                            
                                                      















                                                                                       
                 
                          
                 




                                                        
                                                                        
                                                          

                        

         




                                       

                                             



                                                                      
                             

                              

         




                                             

                                                   


                                           
                                                         








                                                                                   

         




                                       

                                          









                                                          








                                                                                           






                                               


                                                                








                                                            

         




                                            

                                               

                                           
             
                                                         






                                                                                                           

         




                                       

                                                  

                                           
             


                                                         


                        

         




                                       

                                           

                                           
             
                                                         






                                                               

         




                                            

                                                  

                                           
             
                                                         





                                                                                                           

         




                                        

                                           

                                                         
 
                                                     

















                                                                                             
                                                                                    
 





                                                                                 
                                                                    

                                                                                                 
                                                          
                                                    


                        

         




                                       

                                           


                              

         




                                       

                                          
                                                          
                                                                      

                                                              
                                                  




                                 
                                   
             

                                                 

                                                                               
                                                             


                        

         




                                         

                                               
                           
                                         
                              

         




                                       

                                          



                                                                                   
                                                            
 

                                                                           
 
                                                                        


                        

         




                                       

                                          



                                                                                         
                                                


                        

         




                                       

                                          




                                                                      

                                                     

                              
                           

                                 
                                             

                                                

                                                                        









                                            
                                        
             
                                                      

                                       
                                                  
 
                                         












                                                                                           

         




                                       

                                          

                                                 


                                  
                                 
             
                                                    
                                                      



                                                     
                                           

                                                        

                                                           
                                                               





                                                                  
                              
             
                                     

                                                  




                        




                                                       

                                          
                                                                    





                                                        




                                        


                                              
                                        


                              




                                       

                                             
                                           
                                                     
                                 

                              

     
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using IRCTokens;

namespace IRCStates
{
    public class Server
    {
        public const string WhoType = "525"; // randomly generated
        private readonly StatefulDecoder _decoder;

        private readonly Dictionary<string, string> _tempCaps;

        public Server(string name)
        {
            Name          = name;
            Registered    = false;
            Modes         = new List<string>();
            Motd          = new List<string>();
            _decoder      = new StatefulDecoder();
            Users         = new Dictionary<string, User>();
            Channels      = new Dictionary<string, Channel>();
            ISupport      = new ISupport();
            HasCap        = false;
            _tempCaps     = new Dictionary<string, string>();
            AvailableCaps = new Dictionary<string, string>();
            AgreedCaps    = new List<string>();
        }

        public string Name { get; set; }
        public string NickName { get; set; }
        public string NickNameLower { get; set; }
        public string UserName { get; set; }
        public string HostName { get; set; }
        public string RealName { get; set; }
        public string Account { get; set; }
        public string Away { get; set; }

        public bool Registered { get; set; }
        public List<string> Modes { get; set; }
        public List<string> Motd { get; set; }
        public Dictionary<string, User> Users { get; set; }
        public Dictionary<string, Channel> Channels { get; set; }
        public Dictionary<string, string> AvailableCaps { get; set; }
        public List<string> AgreedCaps { get; set; }

        public ISupport ISupport { get; set; }
        public bool HasCap { get; set; }

        public override string ToString()
        {
            return $"Server(name={Name})";
        }

        /// <summary>
        /// Use <see cref="ISupport"/>'s case mapping to convert to lowercase
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public string CaseFold(string str)
        {
            return Casemap.CaseFold(ISupport.CaseMapping, str);
        }

        /// <summary>
        /// Is the current nickname this client?
        /// </summary>
        /// <param name="nickname"></param>
        /// <returns></returns>
        private bool IsMe(string nickname)
        {
            return CaseFold(nickname) == NickNameLower;
        }

        /// <summary>
        /// Check for a user - not case sensitive
        /// </summary>
        /// <param name="nickname"></param>
        /// <returns></returns>
        private bool HasUser(string nickname)
        {
            return Users.ContainsKey(CaseFold(nickname));
        }

        /// <summary>
        /// Get existing user by case-insensitive nickname
        /// </summary>
        /// <param name="nickname"></param>
        /// <returns></returns>
        private User GetUser(string nickname)
        {
            return HasUser(nickname) ? Users[CaseFold(nickname)] : null;
        }

        /// <summary>
        /// Create and add user
        /// </summary>
        /// <param name="nickname"></param>
        /// <returns></returns>
        private User AddUser(string nickname)
        {
            var user = CreateUser(nickname);
            Users[CaseFold(nickname)] = user;
            return user;
        }

        /// <summary>
        /// Build a new <see cref="User"/> and update correct case-mapped nick
        /// </summary>
        /// <param name="nickname"></param>
        /// <returns></returns>
        private User CreateUser(string nickname)
        {
            var user = new User();
            user.SetNickName(nickname, CaseFold(nickname));
            return user;
        }

        /// <summary>
        /// Is the channel a valid ISupport type?
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        private bool IsChannel(string target)
        {
            return !string.IsNullOrEmpty(target) &&
                   ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Is the channel known to this client?
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public bool HasChannel(string name)
        {
            return IsChannel(name) && Channels.ContainsKey(CaseFold(name));
        }

        /// <summary>
        /// Get the channel if it's known to us
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private Channel GetChannel(string name)
        {
            return HasChannel(name) ? Channels[CaseFold(name)] : null;
        }

        /// <summary>
        /// Add a <see cref="User"/> to a <see cref="Channel"/>
        /// </summary>
        /// <param name="channel"></param>
        /// <param name="user"></param>
        /// <returns>the <see cref="ChannelUser"/> that was added</returns>
        private ChannelUser UserJoin(Channel channel, User user)
        {
            var channelUser = new ChannelUser();
            user.Channels.Add(CaseFold(channel.Name));
            channel.Users[user.NickNameLower] = channelUser;
            return channelUser;
        }

        /// <summary>
        /// Set own <see cref="NickName"/>, <see cref="UserName"/>, and <see cref="HostName"/>
        /// from a given <see cref="Hostmask"/>
        /// </summary>
        /// <param name="hostmask"></param>
        private void SelfHostmask(Hostmask hostmask)
        {
            NickName = hostmask.NickName;
            if (hostmask.UserName != null) UserName = hostmask.UserName;
            if (hostmask.HostName != null) HostName = hostmask.HostName;
        }

        private void SelfHostmask(string raw)
        {
            SelfHostmask(new Hostmask(raw));
        }

        /// <summary>
        /// Remove a user from a channel. Used to handle PART and KICK
        /// </summary>
        /// <param name="line"></param>
        /// <param name="nickName"></param>
        /// <param name="channelName"></param>
        /// <param name="reasonIndex"></param>
        /// <returns></returns>
        private (Emit, User) UserPart(Line line, string nickName, string channelName, int reasonIndex)
        {
            var emit                                            = new Emit();
            var channelLower                                    = CaseFold(channelName);
            if (line.Params.Count >= reasonIndex + 1) emit.Text = line.Params[reasonIndex];

            User user = null;
            if (HasChannel(channelName))
            {
                var channel = GetChannel(channelName);
                emit.Channel = channel;
                var nickLower = CaseFold(nickName);
                if (HasUser(nickLower))
                {
                    user = Users[nickLower];
                    user.Channels.Remove(channelLower);
                    channel.Users.Remove(nickLower);
                    if (!user.Channels.Any()) Users.Remove(nickLower);
                }

                if (IsMe(nickName))
                {
                    Channels.Remove(channelLower);
                    foreach (var userToRemove in channel.Users.Keys.Select(u => Users[u]))
                    {
                        userToRemove.Channels.Remove(channelLower);
                        if (!userToRemove.Channels.Any()) Users.Remove(userToRemove.NickNameLower);
                    }
                }
            }

            return (emit, user);
        }

        /// <summary>
        /// Update modes on a <see cref="Channel"/> given modes and parameters
        /// </summary>
        /// <param name="channel"></param>
        /// <param name="modes"></param>
        /// <param name="parameters"></param>
        private void SetChannelModes(Channel channel, IEnumerable<(bool, string)> modes, IList<string> parameters)
        {
            foreach (var (add, c) in modes)
            {
                var listMode = ISupport.ChanModes.ListModes.Contains(c);
                if (ISupport.Prefix.Modes.Contains(c))
                {
                    var nicknameLower = CaseFold(parameters.First());
                    parameters.RemoveAt(0);
                    if (!HasUser(nicknameLower)) continue;

                    var channelUser = channel.Users[nicknameLower];
                    if (add)
                    {
                        if (!channelUser.Modes.Contains(c)) channelUser.Modes.Add(c);
                    }
                    else if (channelUser.Modes.Contains(c))
                    {
                        channelUser.Modes.Remove(c);
                    }
                }
                else if (add && (listMode ||
                                 ISupport.ChanModes.SettingBModes.Contains(c) ||
                                 ISupport.ChanModes.SettingCModes.Contains(c)))
                {
                    channel.AddMode(c, parameters.First(), listMode);
                    parameters.RemoveAt(0);
                }
                else if (!add && (listMode || ISupport.ChanModes.SettingBModes.Contains(c)))
                {
                    channel.RemoveMode(c, parameters.First());
                    parameters.RemoveAt(0);
                }
                else if (add)
                {
                    channel.AddMode(c, null, false);
                }
                else
                {
                    channel.RemoveMode(c, null);
                }
            }
        }

        /// <summary>
        /// Handle incoming bytes
        /// </summary>
        /// <param name="data"></param>
        /// <param name="length"></param>
        /// <returns>parsed lines and emits</returns>
        /// <exception cref="ServerDisconnectedException"></exception>
        public IEnumerable<(Line, Emit)> Receive(byte[] data, int length)
        {
            if (data == null) return null;

            var lines = _decoder.Push(data, length);
            if (lines == null) throw new ServerDisconnectedException();

            return lines.Select(l => (l, Parse(l)));
        }

        /// <summary>
        /// Delegate a <see cref="Line"/> to the correct handler
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        public Emit Parse(Line line)
        {
            if (line == null) return null;

            var emit = line.Command switch
            {
                Numeric.RPL_WELCOME       => HandleWelcome(line),
                Numeric.RPL_ISUPPORT      => HandleISupport(line),
                Numeric.RPL_MOTDSTART     => HandleMotd(line),
                Numeric.RPL_MOTD          => HandleMotd(line),
                Commands.Nick             => HandleNick(line),
                Commands.Join             => HandleJoin(line),
                Commands.Part             => HandlePart(line),
                Commands.Kick             => HandleKick(line),
                Commands.Quit             => HandleQuit(line),
                Commands.Error            => HandleError(line),
                Numeric.RPL_NAMREPLY      => HandleNames(line),
                Numeric.RPL_CREATIONTIME  => HandleCreationTime(line),
                Commands.Topic            => HandleTopic(line),
                Numeric.RPL_TOPIC         => HandleTopicNumeric(line),
                Numeric.RPL_TOPICWHOTIME  => HandleTopicTime(line),
                Commands.Mode             => HandleMode(line),
                Numeric.RPL_CHANNELMODEIS => HandleChannelModeIs(line),
                Numeric.RPL_UMODEIS       => HandleUModeIs(line),
                Commands.Privmsg          => HandleMessage(line),
                Commands.Notice           => HandleMessage(line),
                Commands.Tagmsg           => HandleMessage(line),
                Numeric.RPL_VISIBLEHOST   => HandleVisibleHost(line),
                Numeric.RPL_WHOREPLY      => HandleWhoReply(line),
                Numeric.RPL_WHOSPCRPL     => HandleWhox(line),
                Numeric.RPL_WHOISUSER     => HandleWhoIsUser(line),
                Commands.Chghost          => HandleChghost(line),
                Commands.Setname          => HandleSetname(line),
                Commands.Away             => HandleAway(line),
                Commands.Account          => HandleAccount(line),
                Commands.Cap              => HandleCap(line),
                Numeric.RPL_LOGGEDIN      => HandleLoggedIn(line),
                Numeric.RPL_LOGGEDOUT     => HandleLoggedOut(line),
                _                         => null
            };

            if (emit != null)
                emit.Command = line.Command;
            else
                emit = new Emit();

            return emit;
        }

        /// <summary>
        /// Handles SETNAME command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleSetname(Line line)
        {
            var emit          = new Emit();
            var realname      = line.Params[0];
            var nicknameLower = CaseFold(line.Hostmask.NickName);

            if (IsMe(nicknameLower))
            {
                emit.Self = true;
                RealName  = realname;
            }

            if (Users.ContainsKey(nicknameLower))
            {
                var user = Users[nicknameLower];
                emit.User     = user;
                user.RealName = realname;
            }

            return emit;
        }

        /// <summary>
        /// Handles AWAY command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleAway(Line line)
        {
            var emit          = new Emit();
            var away          = line.Params.FirstOrDefault();
            var nicknameLower = CaseFold(line.Hostmask.NickName);

            if (IsMe(nicknameLower))
            {
                emit.Self = true;
                Away      = away;
            }

            if (Users.ContainsKey(nicknameLower))
            {
                var user = Users[nicknameLower];
                emit.User = user;
                user.Away = away;
            }

            return emit;
        }

        /// <summary>
        /// Handles ACCOUNT command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleAccount(Line line)
        {
            var emit          = new Emit();
            var account       = line.Params[0].Trim('*');
            var nicknameLower = CaseFold(line.Hostmask.NickName);

            if (IsMe(nicknameLower))
            {
                emit.Self = true;
                Account   = account;
            }

            if (Users.ContainsKey(nicknameLower))
            {
                var user = Users[nicknameLower];
                emit.User    = user;
                user.Account = account;
            }

            return emit;
        }

        /// <summary>
        /// Handles CAP command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleCap(Line line)
        {
            HasCap = true;
            var subcommand = line.Params[1].ToUpperInvariant();
            var multiline  = line.Params[2] == "*";
            var caps       = line.Params[multiline ? 3 : 2];

            var tokens    = new Dictionary<string, string>();
            var tokensStr = new List<string>();
            foreach (var cap in caps.Split(' ', StringSplitOptions.RemoveEmptyEntries))
            {
                tokensStr.Add(cap);
                var kv = cap.Split('=', 2);
                tokens[kv[0]] = kv.Length > 1 ? kv[1] : string.Empty;
            }

            var emit = new Emit {Subcommand = subcommand, Finished = !multiline, Tokens = tokensStr};

            switch (subcommand)
            {
                case "LS":
                    _tempCaps.UpdateWith(tokens);
                    if (!multiline)
                    {
                        AvailableCaps.UpdateWith(_tempCaps);
                        _tempCaps.Clear();
                    }

                    break;
                case "NEW":
                    AvailableCaps.UpdateWith(tokens);
                    break;
                case "DEL":
                    foreach (var key in tokens.Keys.Where(key => AvailableCaps.ContainsKey(key)))
                    {
                        AvailableCaps.Remove(key);
                        if (AgreedCaps.Contains(key)) AgreedCaps.Remove(key);
                    }

                    break;
                case "ACK":
                    foreach (var key in tokens.Keys)
                        if (key.StartsWith('-'))
                        {
                            var k = key.Substring(1);
                            if (AgreedCaps.Contains(k)) AgreedCaps.Remove(k);
                        }
                        else if (!AgreedCaps.Contains(key) && AvailableCaps.ContainsKey(key))
                        {
                            AgreedCaps.Add(key);
                        }

                    break;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_LOGGEDIN numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleLoggedIn(Line line)
        {
            SelfHostmask(new Hostmask(line.Params[1]));
            Account = line.Params[2];
            return new Emit();
        }

        /// <summary>
        /// Handles CHGHOST command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleChghost(Line line)
        {
            var emit          = new Emit();
            var username      = line.Params[0];
            var hostname      = line.Params[1];
            var nicknameLower = CaseFold(line.Hostmask.NickName);

            if (IsMe(nicknameLower))
            {
                emit.Self = true;
                UserName  = username;
                HostName  = hostname;
            }

            if (Users.ContainsKey(nicknameLower))
            {
                var user = Users[nicknameLower];
                emit.User     = user;
                user.UserName = username;
                user.HostName = hostname;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_WHOISUSER numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleWhoIsUser(Line line)
        {
            var emit     = new Emit();
            var nickname = line.Params[1];
            var username = line.Params[2];
            var hostname = line.Params[3];
            var realname = line.Params[5];

            if (IsMe(nickname))
            {
                emit.Self = true;
                UserName  = username;
                HostName  = hostname;
                RealName  = realname;
            }

            if (HasUser(nickname))
            {
                var user = Users[CaseFold(nickname)];
                emit.User     = user;
                user.UserName = username;
                user.HostName = hostname;
                user.RealName = realname;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_WHOSPCRPL numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleWhox(Line line)
        {
            var emit = new Emit();
            if (line.Params[1] == WhoType && line.Params.Count == 8)
            {
                var nickname = line.Params[5];
                var username = line.Params[2];
                var hostname = line.Params[4];
                var realname = line.Params[7];
                var account  = line.Params[6] == "0" ? null : line.Params[6];

                if (IsMe(nickname))
                {
                    emit.Self = true;
                    UserName  = username;
                    HostName  = hostname;
                    RealName  = realname;
                    Account   = account;
                }

                if (HasUser(nickname))
                {
                    var user = Users[CaseFold(nickname)];
                    emit.User     = user;
                    user.UserName = username;
                    user.HostName = hostname;
                    user.RealName = realname;
                    user.Account  = account;
                }
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_WHOREPLY numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleWhoReply(Line line)
        {
            var emit     = new Emit {Target = line.Params[1]};
            var nickname = line.Params[5];
            var username = line.Params[2];
            var hostname = line.Params[3];
            var realname = line.Params[7].Split(' ', 2)[1];

            if (IsMe(nickname))
            {
                emit.Self = true;
                UserName  = username;
                HostName  = hostname;
                RealName  = realname;
            }

            if (HasUser(nickname))
            {
                var user = Users[CaseFold(nickname)];
                emit.User     = user;
                user.UserName = username;
                user.HostName = hostname;
                user.RealName = realname;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_VISIBLEHOST numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleVisibleHost(Line line)
        {
            var split = line.Params[1].Split('@', 2);
            switch (split.Length)
            {
                case 1:
                    HostName = split[0];
                    break;
                case 2:
                    HostName = split[1];
                    UserName = split[0];
                    break;
            }

            return new Emit();
        }

        /// <summary>
        /// Handles PRIVMSG, NOTICE, and TAGMSG commands
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleMessage(Line line)
        {
            var emit                       = new Emit();
            var message                    = line.Params.Count > 1 ? line.Params[1] : null;
            if (message != null) emit.Text = message;

            var nick = CaseFold(line.Hostmask.NickName);
            if (IsMe(nick))
            {
                emit.SelfSource = true;
                SelfHostmask(line.Hostmask);
            }

            var user = GetUser(nick) ?? AddUser(nick);
            emit.User = user;

            if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
            if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;

            var target    = line.Params[0];
            var statusMsg = new List<string>();
            while (target.Length > 0)
            {
                var t = target[0].ToString(CultureInfo.InvariantCulture);
                if (ISupport.StatusMsg.Contains(t))
                {
                    statusMsg.Add(t);
                    target = target.Substring(1);
                }
                else
                {
                    break;
                }
            }

            emit.Target = line.Params[0];

            if (IsChannel(target) && HasChannel(target))
                emit.Channel                       = GetChannel(target);
            else if (IsMe(target)) emit.SelfTarget = true;

            return emit;
        }

        /// <summary>
        /// Handles RPL_UMODEIS numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleUModeIs(Line line)
        {
            foreach (var c in line.Params[1]
                .TrimStart('+')
                .Select(m => m.ToString(CultureInfo.InvariantCulture))
                .Where(m => !Modes.Contains(m)))
                Modes.Add(c);

            return new Emit();
        }

        /// <summary>
        /// Handles RPL_CHANNELMODEIS numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleChannelModeIs(Line line)
        {
            var emit = new Emit();
            if (HasChannel(line.Params[1]))
            {
                var channel = GetChannel(line.Params[1]);
                emit.Channel = channel;
                var modes = line.Params[2]
                    .TrimStart('+')
                    .Select(p => (true, p.ToString(CultureInfo.InvariantCulture)));
                var parameters = line.Params.Skip(3).ToList();
                SetChannelModes(channel, modes, parameters);
            }

            return emit;
        }

        /// <summary>
        /// Handles MODE command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleMode(Line line)
        {
            var emit       = new Emit();
            var target     = line.Params[0];
            var modeString = line.Params[1];
            var parameters = line.Params.Skip(2).ToList();

            var modifier = '+';
            var modes    = new List<(bool, string)>();
            var tokens   = new List<string>();

            foreach (var c in modeString)
                if (new[] {'+', '-'}.Contains(c))
                {
                    modifier = c;
                }
                else
                {
                    modes.Add((modifier == '+', c.ToString(CultureInfo.InvariantCulture)));
                    tokens.Add($"{modifier}{c}");
                }

            emit.Tokens = tokens;

            if (IsMe(target))
            {
                emit.SelfTarget = true;
                foreach (var (add, c) in modes)
                    if (add && !Modes.Contains(c))
                        Modes.Add(c);
                    else if (Modes.Contains(c)) Modes.Remove(c);
            }
            else if (HasChannel(target))
            {
                var channel = GetChannel(CaseFold(target));
                emit.Channel = channel;
                SetChannelModes(channel, modes, parameters);
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_TOPICWHOTIME numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleTopicTime(Line line)
        {
            var emit = new Emit();
            if (HasChannel(line.Params[1]))
            {
                var channel = GetChannel(line.Params[1]);
                emit.Channel        = channel;
                channel.TopicSetter = line.Params[2];
                channel.TopicTime = DateTimeOffset
                    .FromUnixTimeSeconds(int.Parse(line.Params[3], CultureInfo.InvariantCulture)).DateTime;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_TOPIC numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleTopicNumeric(Line line)
        {
            var emit = new Emit();
            if (HasChannel(line.Params[1]))
            {
                var channel = GetChannel(line.Params[1]);
                emit.Channel  = channel;
                channel.Topic = line.Params[2];
            }

            return emit;
        }

        /// <summary>
        /// Handles TOPIC command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleTopic(Line line)
        {
            var emit = new Emit();
            if (HasChannel(line.Params[0]))
            {
                var channel = GetChannel(line.Params[0]);
                emit.Channel        = channel;
                channel.Topic       = line.Params[1];
                channel.TopicSetter = line.Hostmask.ToString();
                channel.TopicTime   = DateTime.UtcNow;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_CREATIONTIME numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleCreationTime(Line line)
        {
            var emit = new Emit();
            if (HasChannel(line.Params[1]))
            {
                var channel = GetChannel(line.Params[1]);
                emit.Channel = channel;
                channel.Created = DateTimeOffset
                    .FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_NAMREPLY numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleNames(Line line)
        {
            var emit = new Emit();
            if (!HasChannel(line.Params[2])) return emit;

            var channel = GetChannel(line.Params[2]);
            emit.Channel = channel;
            var nicknames = line.Params[3].Split(' ', StringSplitOptions.RemoveEmptyEntries);
            var users     = new List<User>();
            emit.Users = users;

            foreach (var nick in nicknames)
            {
                var modes = "";
                foreach (var c in nick)
                {
                    var mode = ISupport.Prefix.FromPrefix(c);
                    if (mode != null)
                        modes += mode;
                    else
                        break;
                }

                var hostmask  = new Hostmask(nick.Substring(modes.Length));
                var user = GetUser(hostmask.NickName) ?? AddUser(hostmask.NickName);

                users.Add(user);
                var channelUser = UserJoin(channel, user);

                if (hostmask.UserName != null) user.UserName = hostmask.UserName;
                if (hostmask.HostName != null) user.HostName = hostmask.HostName;

                if (IsMe(hostmask.NickName)) SelfHostmask(hostmask);

                foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
                    if (!channelUser.Modes.Contains(mode))
                        channelUser.Modes.Add(mode);
            }

            return emit;
        }

        /// <summary>
        /// Handles ERROR command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleError(Line line)
        {
            Users.Clear();
            Channels.Clear();
            return new Emit();
        }

        /// <summary>
        /// Handles QUIT command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleQuit(Line line)
        {
            var emit                         = new Emit();
            var nick                         = line.Hostmask.NickName;
            if (line.Params.Any()) emit.Text = line.Params[0];

            if (IsMe(nick) || line.Source == null)
            {
                emit.Self = true;
                Users.Clear();
                Channels.Clear();
            }
            else if (HasUser(nick))
            {
                var user = GetUser(nick);
                Users.Remove(user.NickNameLower);
                emit.User = user;
                foreach (var channel in user.Channels.Select(c => Channels[c]))
                    channel.Users.Remove(user.NickNameLower);
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_LOGGEDOUT numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleLoggedOut(Line line)
        {
            Account = null;
            SelfHostmask(line.Params[1]);
            return new Emit();
        }

        /// <summary>
        /// Handles KICK command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleKick(Line line)
        {
            var (emit, kicked) = UserPart(line, line.Params[1], line.Params[0], 2);
            if (kicked != null)
            {
                emit.UserTarget = kicked;
                if (IsMe(kicked.NickName)) emit.Self = true;

                var kicker                        = line.Hostmask.NickName;
                if (IsMe(kicker)) emit.SelfSource = true;

                emit.UserSource = GetUser(kicker) ?? CreateUser(kicker);
            }

            return emit;
        }

        /// <summary>
        /// Handles PART command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandlePart(Line line)
        {
            var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
            if (user != null)
            {
                emit.User = user;
                emit.Self = IsMe(user.NickName);
            }

            return emit;
        }

        /// <summary>
        /// Handles JOIN command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleJoin(Line line)
        {
            var extended = line.Params.Count == 3;
            var account  = extended ? line.Params[1].Trim('*') : null;
            var realname = extended ? line.Params[2] : null;
            var emit     = new Emit();

            var channelName = line.Params[0];
            var nick        = line.Hostmask.NickName;

            // handle own join
            if (IsMe(nick))
            {
                emit.Self = true;
                if (!HasChannel(channelName))
                {
                    var channel = new Channel();
                    channel.SetName(channelName, CaseFold(channelName));
                    Channels[CaseFold(channelName)] = channel;
                }

                SelfHostmask(line.Hostmask);
                if (extended)
                {
                    Account  = account;
                    RealName = realname;
                }
            }

            if (HasChannel(channelName))
            {
                var channel = GetChannel(channelName);
                emit.Channel = channel;

                if (!HasUser(nick)) AddUser(nick);

                var user = GetUser(nick);
                emit.User = user;
                if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
                if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
                if (extended)
                {
                    user.Account  = account;
                    user.RealName = realname;
                }

                UserJoin(channel, user);
            }

            return emit;
        }

        /// <summary>
        /// Handles NICK command
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleNick(Line line)
        {
            var newNick = line.Params[0];
            var oldNick = line.Hostmask.NickName;

            var emit = new Emit();

            if (HasUser(oldNick))
            {
                var user         = GetUser(oldNick);
                var oldNickLower = user.NickNameLower;
                var newNickLower = CaseFold(newNick);

                emit.User = user;
                Users.Remove(oldNickLower);
                Users[newNickLower] = user;
                user.SetNickName(newNick, newNickLower);

                foreach (var channelLower in user.Channels)
                {
                    var channel     = GetChannel(channelLower);
                    var channelUser = channel.Users[oldNickLower];
                    channel.Users.Remove(oldNickLower);
                    channel.Users[newNickLower] = channelUser;
                }
            }

            if (IsMe(oldNick))
            {
                emit.Self     = true;
                NickName      = newNick;
                NickNameLower = CaseFold(newNick);
            }

            return emit;
        }

        /// <summary>
        /// Handles RPL_MOTDSTART and RPL_MOTD numerics
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleMotd(Line line)
        {
            if (line.Command == Numeric.RPL_MOTDSTART) Motd.Clear();

            var emit = new Emit {Text = line.Params[1]};
            Motd.Add(line.Params[1]);
            return emit;
        }

        /// <summary>
        /// Handles RPL_ISUPPORT numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleISupport(Line line)
        {
            ISupport = new ISupport();
            ISupport.Parse(line.Params);
            return new Emit();
        }

        /// <summary>
        /// Handles RPL_WELCOME numeric
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private Emit HandleWelcome(Line line)
        {
            NickName      = line.Params[0];
            NickNameLower = CaseFold(line.Params[0]);
            Registered    = true;
            return new Emit();
        }
    }
}