using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using IRCTokens; // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable CommentTypo // ReSharper disable IdentifierTypo namespace IRCStates { public class Server { public const string WhoType = "525"; // randomly generated private readonly StatefulDecoder _decoder; private readonly 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; } // ReSharper disable once InconsistentNaming public ISupport ISupport { get; set; } public bool HasCap { get; set; } public override string ToString() { return $"Server(name={Name})"; } /// /// Use 's case mapping to convert to lowercase /// /// /// public string CaseFold(string str) { return Casemap.CaseFold(ISupport.CaseMapping, str); } /// /// Is the current nickname this client? /// /// /// private bool IsMe(string nickname) { return CaseFold(nickname) == NickNameLower; } /// /// Check for a user - not case-sensitive /// /// /// private bool HasUser(string nickname) { return Users.ContainsKey(CaseFold(nickname)); } /// /// Get existing user by case-insensitive nickname /// /// /// private User GetUser(string nickname) { return HasUser(nickname) ? Users[CaseFold(nickname)] : null; } /// /// Create and add user /// /// /// private User AddUser(string nickname) { var user = CreateUser(nickname); Users[CaseFold(nickname)] = user; return user; } /// /// Build a new and update correct case-mapped nick /// /// /// private User CreateUser(string nickname) { var user = new User(); user.SetNickName(nickname, CaseFold(nickname)); return user; } /// /// Is the channel a valid ISupport type? /// /// /// private bool IsChannel(string target) { return !string.IsNullOrEmpty(target) && ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture)); } /// /// Is the channel known to this client? /// /// /// public bool HasChannel(string name) { return IsChannel(name) && Channels.ContainsKey(CaseFold(name)); } /// /// Get the channel if it's known to us /// /// /// private Channel GetChannel(string name) { return HasChannel(name) ? Channels[CaseFold(name)] : null; } /// /// Add a to a /// /// /// /// the that was added private ChannelUser UserJoin(Channel channel, User user) { var channelUser = new ChannelUser(); user.Channels.Add(CaseFold(channel.Name)); channel.Users[user.NickNameLower] = channelUser; return channelUser; } /// /// Set own , , and /// from a given /// /// 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)); } /// /// Remove a user from a channel. Used to handle PART and KICK /// /// /// /// /// /// 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); } /// /// Update modes on a given modes and parameters /// /// /// /// 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); } } } /// /// Handle incoming bytes /// /// /// /// parsed lines and emits /// 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))); } /// /// Delegate a to the correct handler /// /// /// 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; } /// /// Handles SETNAME command /// /// /// 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.TryGetValue(nicknameLower, out var user)) { emit.User = user; user.RealName = realname; } return emit; } /// /// Handles AWAY command /// /// /// 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.TryGetValue(nicknameLower, out var user)) { emit.User = user; user.Away = away; } return emit; } /// /// Handles ACCOUNT command /// /// /// 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.TryGetValue(nicknameLower, out var user)) { emit.User = user; user.Account = account; } return emit; } /// /// Handles CAP command /// /// /// 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[1..]; if (AgreedCaps.Contains(k)) AgreedCaps.Remove(k); } else if (!AgreedCaps.Contains(key) && AvailableCaps.ContainsKey(key)) { AgreedCaps.Add(key); } break; } return emit; } /// /// Handles RPL_LOGGEDIN numeric /// /// /// private Emit HandleLoggedIn(Line line) { SelfHostmask(new Hostmask(line.Params[1])); Account = line.Params[2]; return new Emit(); } /// /// Handles CHGHOST command /// /// /// 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.TryGetValue(nicknameLower, out var user)) { emit.User = user; user.UserName = username; user.HostName = hostname; } return emit; } /// /// Handles RPL_WHOISUSER numeric /// /// /// 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; } /// /// Handles RPL_WHOSPCRPL numeric /// /// /// 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; } /// /// Handles RPL_WHOREPLY numeric /// /// /// 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; } /// /// Handles RPL_VISIBLEHOST numeric /// /// /// 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(); } /// /// Handles PRIVMSG, NOTICE, and TAGMSG commands /// /// /// 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(); while (target.Length > 0) { var t = target[0].ToString(CultureInfo.InvariantCulture); if (ISupport.StatusMsg.Contains(t)) { statusMsg.Add(t); target = target[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; } /// /// Handles RPL_UMODEIS numeric /// /// /// 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(); } /// /// Handles RPL_CHANNELMODEIS numeric /// /// /// 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; } /// /// Handles MODE command /// /// /// 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; } /// /// Handles RPL_TOPICWHOTIME numeric /// /// /// 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; } /// /// Handles RPL_TOPIC numeric /// /// /// 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; } /// /// Handles TOPIC command /// /// /// 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; } /// /// Handles RPL_CREATIONTIME numeric /// /// /// 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; } /// /// Handles RPL_NAMREPLY numeric /// /// /// 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(); 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[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; } /// /// Handles ERROR command /// /// /// private Emit HandleError(Line line) { Users.Clear(); Channels.Clear(); return new Emit(); } /// /// Handles QUIT command /// /// /// 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; } /// /// Handles RPL_LOGGEDOUT numeric /// /// /// private Emit HandleLoggedOut(Line line) { Account = null; SelfHostmask(line.Params[1]); return new Emit(); } /// /// Handles KICK command /// /// /// 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; } /// /// Handles PART command /// /// /// 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; } /// /// Handles JOIN command /// /// /// 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; } /// /// Handles NICK command /// /// /// 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; } /// /// Handles RPL_MOTDSTART and RPL_MOTD numerics /// /// /// 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; } /// /// Handles RPL_ISUPPORT numeric /// /// /// private Emit HandleISupport(Line line) { ISupport = new ISupport(); ISupport.Parse(line.Params); return new Emit(); } /// /// Handles RPL_WELCOME numeric /// /// /// private Emit HandleWelcome(Line line) { NickName = line.Params[0]; NickNameLower = CaseFold(line.Params[0]); Registered = true; return new Emit(); } } }