diff options
Diffstat (limited to 'IRCStates/Server.cs')
-rw-r--r-- | IRCStates/Server.cs | 941 |
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(); + } + } +} |