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 TempCaps; public Server(string name) { Name = name; Registered = false; Modes = new List(); Motd = new List(); _decoder = new StatefulDecoder(); Users = new Dictionary(); Channels = new Dictionary(); ISupport = new ISupport(); HasCap = false; TempCaps = new Dictionary(); AvailableCaps = new Dictionary(); AgreedCaps = new List(); } 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 Modes { get; set; } public List Motd { get; set; } public Dictionary Users { get; set; } public Dictionary Channels { get; set; } public Dictionary AvailableCaps { get; set; } public List 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 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(); var tokensStr = new List(); 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(); 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(); 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(); 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(); } } }