about summary refs log tree commit diff
path: root/IRCStates/Server.cs
diff options
context:
space:
mode:
Diffstat (limited to 'IRCStates/Server.cs')
-rw-r--r--IRCStates/Server.cs941
1 files changed, 941 insertions, 0 deletions
diff --git a/IRCStates/Server.cs b/IRCStates/Server.cs
new file mode 100644
index 0000000..2615080
--- /dev/null
+++ b/IRCStates/Server.cs
@@ -0,0 +1,941 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Design;
+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 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})";
+        }
+
+        public string CaseFold(string str)
+        {
+            return Casemap.CaseFold(ISupport.CaseMapping, str);
+        }
+
+        private bool CaseFoldEquals(string s1, string s2)
+        {
+            return CaseFold(s1) == CaseFold(s2);
+        }
+
+        private bool IsMe(string nickname)
+        {
+            return CaseFold(nickname) == NickNameLower;
+        }
+
+        private bool HasUser(string nickname)
+        {
+            return Users.ContainsKey(CaseFold(nickname));
+        }
+
+        private User AddUser(string nickname, string nicknameLower)
+        {
+            var user = CreateUser(nickname, nicknameLower);
+            Users[nicknameLower] = user;
+            return user;
+        }
+
+        private User CreateUser(string nickname, string nicknameLower)
+        {
+            var user = new User();
+            user.SetNickName(nickname, nicknameLower);
+            return user;
+        }
+
+        private bool IsChannel(string target)
+        {
+            return !string.IsNullOrEmpty(target) &&
+                   ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
+        }
+
+        public bool HasChannel(string name)
+        {
+            return Channels.ContainsKey(CaseFold(name));
+        }
+
+        private Channel GetChannel(string name)
+        {
+            return HasChannel(name) ? Channels[name] : null;
+        }
+
+        private ChannelUser UserJoin(Channel channel, User user)
+        {
+            var channelUser = new ChannelUser();
+            user.Channels.Add(CaseFold(channel.Name));
+            channel.Users[user.NickNameLower] = channelUser;
+            return channelUser;
+        }
+
+        private void SelfHostmask(Hostmask hostmask)
+        {
+            NickName = hostmask.NickName;
+            if (hostmask.UserName != null) UserName = hostmask.UserName;
+            if (hostmask.HostName != null) HostName = hostmask.HostName;
+        }
+
+        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 = Channels[channelLower];
+                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);
+        }
+
+        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);
+                }
+            }
+        }
+
+        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))).ToList();
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        private Emit HandleLoggedIn(Line line)
+        {
+            SelfHostmask(new Hostmask(line.Params[1]));
+            Account = line.Params[2];
+            return new Emit();
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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;
+        }
+
+        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();
+        }
+
+        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 nickLower = CaseFold(line.Hostmask.NickName);
+            if (IsMe(nickLower))
+            {
+                emit.SelfSource = true;
+                SelfHostmask(line.Hostmask);
+            }
+
+            var user = HasUser(nickLower)
+                ? Users[nickLower]
+                : AddUser(line.Hostmask.NickName, nickLower);
+            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 = Channels[CaseFold(target)];
+            }
+            else if (IsMe(target))
+            {
+                emit.SelfTarget = true;
+            }
+
+            return emit;
+        }
+
+        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();
+        }
+
+        private Emit HandleChannelModeIs(Line line)
+        {
+            var emit = new Emit();
+            if (HasChannel(line.Params[1]))
+            {
+                var channel = Channels[CaseFold(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;
+        }
+
+        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;
+        }
+
+        private Emit HandleTopicTime(Line line)
+        {
+            var emit         = new Emit();
+            var channelLower = CaseFold(line.Params[1]);
+            if (Channels.ContainsKey(channelLower))
+            {
+                var channel = Channels[channelLower];
+                emit.Channel        = channel;
+                channel.TopicSetter = line.Params[2];
+                channel.TopicTime = DateTimeOffset
+                    .FromUnixTimeSeconds(int.Parse(line.Params[3], CultureInfo.InvariantCulture)).DateTime;
+            }
+
+            return emit;
+        }
+
+        private Emit HandleTopicNumeric(Line line)
+        {
+            var emit         = new Emit();
+            var channelLower = CaseFold(line.Params[1]);
+            if (Channels.ContainsKey(channelLower))
+            {
+                var channel = Channels[channelLower];
+                emit.Channel                 = channel;
+                Channels[channelLower].Topic = line.Params[2];
+            }
+
+            return emit;
+        }
+
+        private Emit HandleTopic(Line line)
+        {
+            var emit         = new Emit();
+            var channelLower = CaseFold(line.Params[0]);
+            if (Channels.ContainsKey(channelLower))
+            {
+                var channel = Channels[channelLower];
+                emit.Channel        = channel;
+                channel.Topic       = line.Params[1];
+                channel.TopicSetter = line.Hostmask.ToString();
+                channel.TopicTime   = DateTime.UtcNow;
+            }
+
+            return emit;
+        }
+
+        private Emit HandleCreationTime(Line line)
+        {
+            var emit         = new Emit();
+            var channelLower = CaseFold(line.Params[1]);
+            if (Channels.ContainsKey(channelLower))
+            {
+                var channel = Channels[channelLower];
+                emit.Channel = channel;
+                channel.Created = DateTimeOffset
+                    .FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
+            }
+
+            return emit;
+        }
+
+        private Emit HandleNames(Line line)
+        {
+            var emit         = new Emit();
+            var channelLower = CaseFold(line.Params[2]);
+
+            if (!Channels.ContainsKey(channelLower)) return emit;
+            var channel = Channels[channelLower];
+            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 nickLower = CaseFold(hostmask.NickName);
+                if (!Users.ContainsKey(nickLower)) AddUser(hostmask.NickName, nickLower);
+
+                var user = Users[nickLower];
+                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(nickLower)) SelfHostmask(hostmask);
+
+                foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
+                    if (!channelUser.Modes.Contains(mode))
+                        channelUser.Modes.Add(mode);
+            }
+
+            return emit;
+        }
+
+        private Emit HandleError(Line line)
+        {
+            Users.Clear();
+            Channels.Clear();
+            return new Emit();
+        }
+
+        private Emit HandleQuit(Line line)
+        {
+            var emit                         = new Emit();
+            var nickLower                    = CaseFold(line.Hostmask.NickName);
+            if (line.Params.Any()) emit.Text = line.Params[0];
+
+            if (IsMe(nickLower) || line.Source == null)
+            {
+                emit.Self = true;
+                Users.Clear();
+                Channels.Clear();
+            }
+            else if (Users.ContainsKey(nickLower))
+            {
+                var user = Users[nickLower];
+                Users.Remove(nickLower);
+                emit.User = user;
+                foreach (var channel in user.Channels.Select(c => Channels[c]))
+                    channel.Users.Remove(user.NickNameLower);
+            }
+
+            return emit;
+        }
+
+        private Emit HandleLoggedOut(Line line)
+        {
+            Account = null;
+            SelfHostmask(new Hostmask(line.Params[1]));
+            return new Emit();
+        }
+
+        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 kickerLower                        = CaseFold(line.Hostmask.NickName);
+                if (IsMe(kickerLower)) emit.SelfSource = true;
+
+                emit.UserSource = Users.ContainsKey(kickerLower)
+                    ? Users[kickerLower]
+                    : CreateUser(line.Hostmask.NickName, kickerLower);
+            }
+
+            return emit;
+        }
+
+        private Emit HandlePart(Line line)
+        {
+            var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
+            if (user != null)
+            {
+                emit.User = user;
+                if (IsMe(user.NickName)) emit.Self = true;
+            }
+
+            return emit;
+        }
+
+
+        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 channelLower = CaseFold(line.Params[0]);
+            var nickLower    = CaseFold(line.Hostmask.NickName);
+
+            // handle own join
+            if (IsMe(nickLower))
+            {
+                emit.Self = true;
+                if (!HasChannel(channelLower))
+                {
+                    var channel = new Channel();
+                    channel.SetName(line.Params[0], channelLower);
+                    Channels[channelLower] = channel;
+                }
+
+                SelfHostmask(line.Hostmask);
+                if (extended)
+                {
+                    Account  = account;
+                    RealName = realname;
+                }
+            }
+
+            if (HasChannel(channelLower))
+            {
+                var channel = Channels[channelLower];
+                emit.Channel = channel;
+
+                if (!HasUser(nickLower)) AddUser(line.Hostmask.NickName, nickLower);
+
+                var user = Users[nickLower];
+                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;
+        }
+
+
+        private Emit HandleNick(Line line)
+        {
+            var nick      = line.Params[0];
+            var nickLower = CaseFold(line.Hostmask.NickName);
+
+            var emit = new Emit();
+
+            if (Users.ContainsKey(nickLower))
+            {
+                var user = Users[nickLower];
+                Users.Remove(nickLower);
+                emit.User = user;
+
+                var oldNickLower = user.NickNameLower;
+                var newNickLower = CaseFold(nick);
+                user.SetNickName(nick, newNickLower);
+                Users[newNickLower] = user;
+                foreach (var channelLower in user.Channels)
+                {
+                    var channel     = Channels[channelLower];
+                    var channelUser = channel.Users[oldNickLower];
+                    channel.Users.Remove(oldNickLower);
+                    channel.Users[newNickLower] = channelUser;
+                }
+            }
+
+            if (IsMe(nickLower))
+            {
+                emit.Self     = true;
+                NickName      = nick;
+                NickNameLower = CaseFold(nick);
+            }
+
+            return emit;
+        }
+
+        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;
+        }
+
+        private Emit HandleISupport(Line line)
+        {
+            ISupport = new ISupport();
+            ISupport.Parse(line.Params);
+            return new Emit();
+        }
+
+
+        private Emit HandleWelcome(Line line)
+        {
+            NickName      = line.Params[0];
+            NickNameLower = CaseFold(line.Params[0]);
+            Registered    = true;
+            return new Emit();
+        }
+    }
+}