From 21f1e95fb8e935134a969bc3d729964d8d2aadfa Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Thu, 14 May 2020 23:06:10 -0400 Subject: rename Irc to IRC --- IRCStates/Casemap.cs | 38 ++ IRCStates/Channel.cs | 67 +++ IRCStates/ChannelUser.cs | 32 ++ IRCStates/Commands.cs | 22 + IRCStates/Emit.cs | 24 + IRCStates/Extensions.cs | 15 + IRCStates/IRCStates.csproj | 21 + IRCStates/ISupport.cs | 114 ++++ IRCStates/ISupportChanModes.cs | 33 ++ IRCStates/ISupportPrefix.cs | 44 ++ IRCStates/Numeric.cs | 54 ++ IRCStates/README.md | 82 +++ IRCStates/Server.cs | 941 +++++++++++++++++++++++++++++++ IRCStates/ServerDisconnectedException.cs | 8 + IRCStates/ServerException.cs | 8 + IRCStates/Tests/Cap.cs | 131 +++++ IRCStates/Tests/Casemap.cs | 58 ++ IRCStates/Tests/Channel.cs | 202 +++++++ IRCStates/Tests/Emit.cs | 117 ++++ IRCStates/Tests/ISupport.cs | 210 +++++++ IRCStates/Tests/Mode.cs | 179 ++++++ IRCStates/Tests/Motd.cs | 23 + IRCStates/Tests/Sasl.cs | 38 ++ IRCStates/Tests/User.cs | 298 ++++++++++ IRCStates/Tests/Who.cs | 61 ++ IRCStates/User.cs | 33 ++ 26 files changed, 2853 insertions(+) create mode 100644 IRCStates/Casemap.cs create mode 100644 IRCStates/Channel.cs create mode 100644 IRCStates/ChannelUser.cs create mode 100644 IRCStates/Commands.cs create mode 100644 IRCStates/Emit.cs create mode 100644 IRCStates/Extensions.cs create mode 100644 IRCStates/IRCStates.csproj create mode 100644 IRCStates/ISupport.cs create mode 100644 IRCStates/ISupportChanModes.cs create mode 100644 IRCStates/ISupportPrefix.cs create mode 100644 IRCStates/Numeric.cs create mode 100644 IRCStates/README.md create mode 100644 IRCStates/Server.cs create mode 100644 IRCStates/ServerDisconnectedException.cs create mode 100644 IRCStates/ServerException.cs create mode 100644 IRCStates/Tests/Cap.cs create mode 100644 IRCStates/Tests/Casemap.cs create mode 100644 IRCStates/Tests/Channel.cs create mode 100644 IRCStates/Tests/Emit.cs create mode 100644 IRCStates/Tests/ISupport.cs create mode 100644 IRCStates/Tests/Mode.cs create mode 100644 IRCStates/Tests/Motd.cs create mode 100644 IRCStates/Tests/Sasl.cs create mode 100644 IRCStates/Tests/User.cs create mode 100644 IRCStates/Tests/Who.cs create mode 100644 IRCStates/User.cs (limited to 'IRCStates') diff --git a/IRCStates/Casemap.cs b/IRCStates/Casemap.cs new file mode 100644 index 0000000..4546e57 --- /dev/null +++ b/IRCStates/Casemap.cs @@ -0,0 +1,38 @@ +using System; + +namespace IRCStates +{ + public static class Casemap + { + public enum CaseMapping + { + Rfc1459, + Ascii + } + + private const string AsciiUpperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private const string AsciiLowerChars = "abcdefghijklmnopqrstuvwxyz"; + private const string Rfc1459UpperChars = AsciiUpperChars + @"[]~\"; + private const string Rfc1459LowerChars = AsciiLowerChars + @"{}^|"; + + private static string Replace(string s, string upper, string lower) + { + for (var i = 0; i < upper.Length; ++i) s = s.Replace(upper[i], lower[i]); + + return s; + } + + public static string CaseFold(CaseMapping mapping, string s) + { + if (s != null) + return mapping switch + { + CaseMapping.Rfc1459 => Replace(s, Rfc1459UpperChars, Rfc1459LowerChars), + CaseMapping.Ascii => Replace(s, AsciiUpperChars, AsciiLowerChars), + _ => throw new ArgumentOutOfRangeException(nameof(mapping), mapping, null) + }; + + return string.Empty; + } + } +} diff --git a/IRCStates/Channel.cs b/IRCStates/Channel.cs new file mode 100644 index 0000000..60ca3fb --- /dev/null +++ b/IRCStates/Channel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IRCStates +{ + public class Channel + { + public Channel() + { + Users = new Dictionary(); + ListModes = new Dictionary>(); + Modes = new Dictionary(); + } + + public string Name { get; set; } + public string NameLower { get; set; } + public Dictionary Users { get; set; } + public string Topic { get; set; } + public string TopicSetter { get; set; } + public DateTime TopicTime { get; set; } + public DateTime Created { get; set; } + public Dictionary> ListModes { get; set; } + public Dictionary Modes { get; set; } + + public override string ToString() + { + return $"Channel(name={Name})"; + } + + public void SetName(string name, string nameLower) + { + Name = name; + NameLower = nameLower; + } + + public void AddMode(string ch, string param, bool listMode) + { + if (listMode) + { + if (!ListModes.ContainsKey(ch)) ListModes[ch] = new List(); + + if (!ListModes[ch].Contains(param)) ListModes[ch].Add(param ?? string.Empty); + } + else + { + Modes[ch] = param; + } + } + + public void RemoveMode(string ch, string param) + { + if (ListModes.ContainsKey(ch)) + { + if (ListModes[ch].Contains(param)) + { + ListModes[ch].Remove(param); + if (!ListModes[ch].Any()) ListModes.Remove(ch); + } + } + else if (Modes.ContainsKey(ch)) + { + Modes.Remove(ch); + } + } + } +} diff --git a/IRCStates/ChannelUser.cs b/IRCStates/ChannelUser.cs new file mode 100644 index 0000000..8c2298b --- /dev/null +++ b/IRCStates/ChannelUser.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace IRCStates +{ + public class ChannelUser + { + public ChannelUser() + { + Modes = new List(); + } + + public List Modes { get; set; } + + protected bool Equals(ChannelUser other) + { + return other != null && Equals(Modes, other.Modes); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ChannelUser) obj); + } + + public override int GetHashCode() + { + return Modes != null ? Modes.GetHashCode() : 0; + } + } +} diff --git a/IRCStates/Commands.cs b/IRCStates/Commands.cs new file mode 100644 index 0000000..d9654ec --- /dev/null +++ b/IRCStates/Commands.cs @@ -0,0 +1,22 @@ +namespace IRCStates +{ + public static class Commands + { + public const string Nick = "NICK"; + public const string Join = "JOIN"; + public const string Mode = "MODE"; + public const string Part = "PART"; + public const string Kick = "KICK"; + public const string Quit = "QUIT"; + public const string Error = "ERROR"; + public const string Topic = "TOPIC"; + public const string Privmsg = "PRIVMSG"; + public const string Notice = "NOTICE"; + public const string Tagmsg = "TAGMSG"; + public const string Chghost = "CHGHOST"; + public const string Setname = "SETNAME"; + public const string Away = "AWAY"; + public const string Account = "ACCOUNT"; + public const string Cap = "CAP"; + } +} diff --git a/IRCStates/Emit.cs b/IRCStates/Emit.cs new file mode 100644 index 0000000..a5f1af5 --- /dev/null +++ b/IRCStates/Emit.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace IRCStates +{ + public class Emit + { + public string Command { get; set; } + public string Subcommand { get; set; } + public string Text { get; set; } + public List Tokens { get; set; } + public bool Finished { get; set; } + public bool Self { get; set; } + public bool SelfSource { get; set; } + public bool SelfTarget { get; set; } + public User User { get; set; } + public User UserSource { get; set; } + public User UserTarget { get; set; } + public List Users { get; set; } + public Channel Channel { get; set; } + public Channel ChannelSource { get; set; } + public Channel ChannelTarget { get; set; } + public string Target { get; set; } + } +} diff --git a/IRCStates/Extensions.cs b/IRCStates/Extensions.cs new file mode 100644 index 0000000..c807dbb --- /dev/null +++ b/IRCStates/Extensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; + +namespace IRCStates +{ + public static class Extensions + { + public static void UpdateWith(this Dictionary dict, Dictionary other) + { + if (dict == null || other == null || !other.Any()) return; + + foreach (var (key, value) in other) dict[key] = value; + } + } +} diff --git a/IRCStates/IRCStates.csproj b/IRCStates/IRCStates.csproj new file mode 100644 index 0000000..cf9f190 --- /dev/null +++ b/IRCStates/IRCStates.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/IRCStates/ISupport.cs b/IRCStates/ISupport.cs new file mode 100644 index 0000000..5fcd5b1 --- /dev/null +++ b/IRCStates/ISupport.cs @@ -0,0 +1,114 @@ +// ReSharper disable InconsistentNaming + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace IRCStates +{ + public class ISupport + { + public ISupport() + { + Raw = new Dictionary(); + Modes = 3; + CaseMapping = Casemap.CaseMapping.Rfc1459; + Prefix = new ISupportPrefix("(ov)@+"); + ChanModes = new ISupportChanModes("b,k,l,imnpst"); + ChanTypes = new List {"#"}; + StatusMsg = new List(); + Whox = false; + } + + public Dictionary Raw { get; set; } + public string Network { get; set; } + public ISupportChanModes ChanModes { get; set; } + public ISupportPrefix Prefix { get; set; } + public int? Modes { get; set; } + public Casemap.CaseMapping CaseMapping { get; set; } + public List ChanTypes { get; set; } + public List StatusMsg { get; set; } + public string CallerId { get; set; } + public string Excepts { get; set; } + public string Invex { get; set; } + public int? Monitor { get; set; } + public int? Watch { get; set; } + public bool Whox { get; set; } + + public void Parse(IEnumerable tokens) + { + if (tokens == null) return; + + // remove first and last + tokens = tokens.Skip(1).SkipLast(1); + + foreach (var token in tokens) + { + var split = token.Split('=', 2); + var key = split[0]; + + var value = string.Empty; + if (split.Length > 1) + { + value = split[1]; + Raw[key] = value; + } + + switch (split[0]) + { + case "NETWORK": + Network = value; + break; + case "CHANMODES": + ChanModes = new ISupportChanModes(value); + break; + case "PREFIX": + Prefix = new ISupportPrefix(value); + break; + case "STATUSMSG": + StatusMsg = new List(); + StatusMsg.AddRange(value.Select(c => c.ToString(CultureInfo.InvariantCulture))); + break; + case "MODES": + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var modes)) + Modes = modes; + else + Modes = -1; + break; + case "MONITOR": + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var monitor)) + Monitor = monitor; + else + Monitor = -1; + break; + case "WATCH": + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var watch)) + Watch = watch; + else + Watch = -1; + break; + case "CASEMAPPING": + if (Enum.TryParse(value, true, out Casemap.CaseMapping caseMapping)) CaseMapping = caseMapping; + break; + case "CHANTYPES": + ChanTypes = new List(); + ChanTypes.AddRange(value.Select(c => c.ToString(CultureInfo.InvariantCulture))); + break; + case "CALLERID": + CallerId = string.IsNullOrEmpty(value) ? "g" : value; + break; + case "EXCEPTS": + Excepts = string.IsNullOrEmpty(value) ? "e" : value; + break; + case "INVEX": + Invex = string.IsNullOrEmpty(value) ? "I" : value; + break; + case "WHOX": + Whox = true; + break; + } + } + } + } +} diff --git a/IRCStates/ISupportChanModes.cs b/IRCStates/ISupportChanModes.cs new file mode 100644 index 0000000..68cfa67 --- /dev/null +++ b/IRCStates/ISupportChanModes.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace IRCStates +{ + public class ISupportChanModes + { + public ISupportChanModes(string splitVal) + { + if (splitVal == null) return; + + var split = splitVal.Split(',', 4); + + ListModes = new List(); + ListModes.AddRange(split[0].Select(c => c.ToString(CultureInfo.InvariantCulture))); + + SettingBModes = new List(); + SettingBModes.AddRange(split[1].Select(c => c.ToString(CultureInfo.InvariantCulture))); + + SettingCModes = new List(); + SettingCModes.AddRange(split[2].Select(c => c.ToString(CultureInfo.InvariantCulture))); + + SettingDModes = new List(); + SettingDModes.AddRange(split[3].Select(c => c.ToString(CultureInfo.InvariantCulture))); + } + + public List ListModes { get; set; } + public List SettingBModes { get; set; } + public List SettingCModes { get; set; } + public List SettingDModes { get; set; } + } +} diff --git a/IRCStates/ISupportPrefix.cs b/IRCStates/ISupportPrefix.cs new file mode 100644 index 0000000..35c5344 --- /dev/null +++ b/IRCStates/ISupportPrefix.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace IRCStates +{ + public class ISupportPrefix + { + public ISupportPrefix(string splitVal) + { + if (splitVal == null) throw new ArgumentNullException(nameof(splitVal)); + + var split = splitVal.Substring(1).Split(')', 2); + Modes = new List(); + Modes.AddRange(split[0].Select(c => c.ToString(CultureInfo.InvariantCulture))); + Prefixes = new List(); + Prefixes.AddRange(split[1].Select(c => c.ToString(CultureInfo.InvariantCulture))); + } + + public List Modes { get; set; } + public List Prefixes { get; set; } + + public string FromMode(char mode) + { + return FromMode(mode.ToString(CultureInfo.InvariantCulture)); + } + + public string FromMode(string mode) + { + return Modes.Contains(mode) ? Prefixes[Modes.IndexOf(mode)] : null; + } + + public string FromPrefix(char prefix) + { + return FromPrefix(prefix.ToString(CultureInfo.InvariantCulture)); + } + + public string FromPrefix(string prefix) + { + return Prefixes.Contains(prefix) ? Modes[Prefixes.IndexOf(prefix)] : null; + } + } +} diff --git a/IRCStates/Numeric.cs b/IRCStates/Numeric.cs new file mode 100644 index 0000000..1ccbd76 --- /dev/null +++ b/IRCStates/Numeric.cs @@ -0,0 +1,54 @@ +// ReSharper disable InconsistentNaming + +namespace IRCStates +{ + public static class Numeric + { +#pragma warning disable CA1707 // Identifiers should not contain underscores + public const string RPL_WELCOME = "001"; + public const string RPL_ISUPPORT = "005"; + public const string RPL_MOTD = "372"; + public const string RPL_MOTDSTART = "375"; + public const string RPL_UMODEIS = "221"; + public const string RPL_VISIBLEHOST = "396"; + + public const string RPL_CHANNELMODEIS = "324"; + public const string RPL_CREATIONTIME = "329"; + public const string RPL_TOPIC = "332"; + public const string RPL_TOPICWHOTIME = "333"; + + public const string RPL_WHOREPLY = "352"; + public const string RPL_WHOSPCRPL = "354"; + public const string RPL_ENDOFWHO = "315"; + public const string RPL_NAMREPLY = "353"; + public const string RPL_ENDOFNAMES = "366"; + + public const string RPL_BANLIST = "367"; + public const string RPL_ENDOFBANLIST = "368"; + public const string RPL_QUIETLIST = "728"; + public const string RPL_ENDOFQUIETLIST = "729"; + + public const string RPL_LOGGEDIN = "900"; + public const string RPL_LOGGEDOUT = "901"; + public const string RPL_SASLSUCCESS = "903"; + public const string ERR_SASLFAIL = "904"; + public const string ERR_SASLTOOLONG = "905"; + public const string ERR_SASLABORTED = "906"; + public const string ERR_SASLALREADY = "907"; + public const string RPL_SASLMECHS = "908"; + + public const string RPL_WHOISUSER = "311"; + public const string RPL_WHOISSERVER = "312"; + public const string RPL_WHOISOPERATOR = "313"; + public const string RPL_WHOISIDLE = "317"; + public const string RPL_WHOISCHANNELS = "319"; + public const string RPL_WHOISACCOUNT = "330"; + public const string RPL_WHOISHOST = "378"; + public const string RPL_WHOISMODES = "379"; + public const string RPL_WHOISSECURE = "671"; + public const string RPL_ENDOFWHOIS = "318"; + + public const string ERR_NOSUCHCHANNEL = "403"; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } +} diff --git a/IRCStates/README.md b/IRCStates/README.md new file mode 100644 index 0000000..05daa8c --- /dev/null +++ b/IRCStates/README.md @@ -0,0 +1,82 @@ +# IrcStates + +port of [jesopo/ircstates](https://github.com/jesopo/ircstates) + +bare bones irc client state + +see the full example in [StatesSample/Client.cs](../Examples/States/Client.cs) + + internal class Client + { + private readonly byte[] _bytes; + private readonly StatefulEncoder _encoder; + private readonly string _host; + private readonly string _nick; + private readonly int _port; + private readonly Server _server; + private readonly Socket _socket; + + public Client(string host, int port, string nick) + { + _server = new Server("test"); + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _encoder = new StatefulEncoder(); + _host = host; + _port = port; + _nick = nick; + _bytes = new byte[1024]; + } + + private void Send(string raw) + { + _encoder.Push(new Line(raw)); + } + + public void Start() + { + _socket.Connect(_host, _port); + while (!_socket.Connected) Thread.Sleep(1000); + + Send("USER test 0 * test"); + Send($"NICK {_nick}"); + + while (true) + { + while (_encoder.PendingBytes.Any()) + { + var bytesSent = _socket.Send(_encoder.PendingBytes); + var sentLines = _encoder.Pop(bytesSent); + foreach (var line in sentLines) Console.WriteLine($"> {line.Format()}"); + } + + var bytesReceived = _socket.Receive(_bytes); + if (bytesReceived == 0) + { + Console.WriteLine("! disconnected"); + _socket.Shutdown(SocketShutdown.Both); + _socket.Close(); + break; + } + + var receivedLines = _server.Receive(_bytes, bytesReceived); + foreach (var (line, _) in receivedLines) + { + Console.WriteLine($"< {line.Format()}"); + + switch (line.Command) + { + case Commands.Privmsg: + if (line.Params[1].Contains(_server.NickName)) + Send($"PRIVMSG {line.Params[0]} :hi {line.Hostmask.NickName}!"); + break; + case "PING": + Send($"PONG :{line.Params[0]}"); + break; + case Numeric.RPL_WELCOME: + if (!_server.HasChannel("#test")) Send("JOIN #test"); + break; + } + } + } + } + } \ No newline at end of file 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 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(); + } + } +} diff --git a/IRCStates/ServerDisconnectedException.cs b/IRCStates/ServerDisconnectedException.cs new file mode 100644 index 0000000..4d0bab6 --- /dev/null +++ b/IRCStates/ServerDisconnectedException.cs @@ -0,0 +1,8 @@ +using System; + +namespace IRCStates +{ + public class ServerDisconnectedException : Exception + { + } +} diff --git a/IRCStates/ServerException.cs b/IRCStates/ServerException.cs new file mode 100644 index 0000000..0f44a88 --- /dev/null +++ b/IRCStates/ServerException.cs @@ -0,0 +1,8 @@ +using System; + +namespace IRCStates +{ + public class ServerException : Exception + { + } +} diff --git a/IRCStates/Tests/Cap.cs b/IRCStates/Tests/Cap.cs new file mode 100644 index 0000000..3c0faba --- /dev/null +++ b/IRCStates/Tests/Cap.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Cap + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + } + + [TestMethod] + public void LSOneLine() + { + Assert.IsFalse(_server.HasCap); + CollectionAssert.AreEqual(new Dictionary(), _server.AvailableCaps); + _server.Parse(new Line("CAP * LS :a b")); + CollectionAssert.AreEqual(new Dictionary {{"a", ""}, {"b", ""}}, _server.AvailableCaps); + } + + [TestMethod] + public void LSTwoLines() + { + _server.Parse(new Line("CAP * LS * :a b")); + CollectionAssert.AreEqual(new Dictionary(), _server.AvailableCaps); + _server.Parse(new Line("CAP * LS :c")); + Assert.IsTrue(_server.AvailableCaps.ContainsKey("a")); + Assert.IsTrue(_server.AvailableCaps.ContainsKey("b")); + Assert.IsTrue(_server.AvailableCaps.ContainsKey("c")); + } + + [TestMethod] + public void LSValues() + { + _server.Parse(new Line("CAP * LS :a b= c=1")); + CollectionAssert.AreEqual(new Dictionary {{"a", ""}, {"b", ""}, {"c", "1"}}, + _server.AvailableCaps); + } + + [TestMethod] + public void ACKOneLine() + { + _server.Parse(new Line("CAP * LS :a b")); + _server.Parse(new Line("CAP * ACK :a b")); + CollectionAssert.AreEqual(new List {"a", "b"}, _server.AgreedCaps); + } + + [TestMethod] + public void ACKTwoLines() + { + _server.Parse(new Line("CAP * LS :a b c")); + _server.Parse(new Line("CAP * ACK * :a b")); + _server.Parse(new Line("CAP * ACK :c")); + CollectionAssert.AreEqual(new List {"a", "b", "c"}, _server.AgreedCaps); + } + + [TestMethod] + public void ACKNotLS() + { + _server.Parse(new Line("CAP * LS a")); + _server.Parse(new Line("CAP * ACK b")); + CollectionAssert.AreEqual(new List(), _server.AgreedCaps); + } + + [TestMethod] + public void NEWNoLS() + { + _server.Parse(new Line("CAP * NEW :a")); + CollectionAssert.AreEqual(new Dictionary {{"a", ""}}, _server.AvailableCaps); + } + + [TestMethod] + public void NEWOneLine() + { + _server.Parse(new Line("CAP * LS :a")); + _server.Parse(new Line("CAP * NEW :b")); + CollectionAssert.AreEqual(new Dictionary {{"a", ""}, {"b", ""}}, _server.AvailableCaps); + } + + [TestMethod] + public void NEWTwoLines() + { + _server.Parse(new Line("CAP * LS :a")); + _server.Parse(new Line("CAP * NEW :b c")); + CollectionAssert.AreEqual(new Dictionary {{"a", ""}, {"b", ""}, {"c", ""}}, + _server.AvailableCaps); + } + + [TestMethod] + public void DELNotAcked() + { + _server.Parse(new Line("CAP * DEL a")); + } + + [TestMethod] + public void DELOneLS() + { + _server.Parse(new Line("CAP * LS :a")); + _server.Parse(new Line("CAP * ACK :a")); + _server.Parse(new Line("CAP * DEL :a")); + CollectionAssert.AreEqual(new Dictionary(), _server.AvailableCaps); + CollectionAssert.AreEqual(new List(), _server.AgreedCaps); + } + + [TestMethod] + public void DELTwoLS() + { + _server.Parse(new Line("CAP * LS :a b")); + _server.Parse(new Line("CAP * ACK :a b")); + _server.Parse(new Line("CAP * DEL :a")); + CollectionAssert.AreEqual(new Dictionary {{"b", ""}}, _server.AvailableCaps); + CollectionAssert.AreEqual(new List {"b"}, _server.AgreedCaps); + } + + [TestMethod] + public void DELTwoDEL() + { + _server.Parse(new Line("CAP * LS :a b")); + _server.Parse(new Line("CAP * ACK :a b")); + _server.Parse(new Line("CAP * DEL :a b")); + CollectionAssert.AreEqual(new Dictionary(), _server.AvailableCaps); + CollectionAssert.AreEqual(new List(), _server.AgreedCaps); + } + } +} diff --git a/IRCStates/Tests/Casemap.cs b/IRCStates/Tests/Casemap.cs new file mode 100644 index 0000000..4a02444 --- /dev/null +++ b/IRCStates/Tests/Casemap.cs @@ -0,0 +1,58 @@ +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Casemap + { + [TestMethod] + public void Rfc1459() + { + var lower = IRCStates.Casemap.CaseFold(IRCStates.Casemap.CaseMapping.Rfc1459, @"ÀTEST[]~\"); + Assert.AreEqual("Àtest{}^|", lower); + } + + [TestMethod] + public void Ascii() + { + var lower = IRCStates.Casemap.CaseFold(IRCStates.Casemap.CaseMapping.Ascii, @"ÀTEST[]~\"); + Assert.AreEqual(@"Àtest[]~\", lower); + } + + [TestMethod] + public void CommandJoin() + { + var server = new Server("test"); + server.Parse(new Line("001 nickname")); + server.Parse(new Line(":Nickname JOIN #Chan")); + server.Parse(new Line(":Other JOIN #Chan")); + + Assert.IsTrue(server.Users.ContainsKey("nickname")); + Assert.IsFalse(server.Users.ContainsKey("Nickname")); + Assert.IsTrue(server.Users.ContainsKey("other")); + Assert.IsFalse(server.Users.ContainsKey("Other")); + Assert.IsTrue(server.Channels.ContainsKey("#chan")); + Assert.IsFalse(server.Channels.ContainsKey("#Chan")); + + var channel = server.Channels["#chan"]; + Assert.AreEqual("#Chan", channel.Name); + } + + [TestMethod] + public void CommandNick() + { + var server = new Server("test"); + server.Parse(new Line("001 nickname")); + server.Parse(new Line(":nickname JOIN #chan")); + var user = server.Users["nickname"]; + server.Parse(new Line(":nickname NICK NewNickname")); + Assert.AreEqual(1, server.Users.Count); + Assert.IsTrue(server.Users.ContainsKey("newnickname")); + Assert.AreEqual("NewNickname", user.NickName); + Assert.AreEqual("newnickname", user.NickNameLower); + Assert.AreEqual("NewNickname", server.NickName); + Assert.AreEqual("newnickname", server.NickNameLower); + } + } +} diff --git a/IRCStates/Tests/Channel.cs b/IRCStates/Tests/Channel.cs new file mode 100644 index 0000000..6868e0f --- /dev/null +++ b/IRCStates/Tests/Channel.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Channel + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + _server.Parse(new Line(":nickname JOIN #chan")); + } + + [TestMethod] + public void JoinSelf() + { + Assert.IsTrue(_server.Channels.ContainsKey("#chan")); + Assert.IsTrue(_server.Users.ContainsKey("nickname")); + Assert.AreEqual(1, _server.Channels.Count); + Assert.AreEqual(1, _server.Users.Count); + + var user = _server.Users["nickname"]; + var chan = _server.Channels["#chan"]; + Assert.IsTrue(chan.Users.ContainsKey(user.NickNameLower)); + var chanUser = chan.Users[user.NickNameLower]; + CollectionAssert.AreEqual(new List {chan.NameLower}, user.Channels.ToList()); + } + + [TestMethod] + public void JoinOther() + { + _server.Parse(new Line(":other JOIN #chan")); + + Assert.AreEqual(2, _server.Users.Count); + Assert.IsTrue(_server.Users.ContainsKey("other")); + + var channel = _server.Channels["#chan"]; + Assert.AreEqual(2, channel.Users.Count); + + var user = _server.Users["other"]; + CollectionAssert.AreEqual(new List {channel.NameLower}, user.Channels.ToList()); + } + + [TestMethod] + public void PartSelf() + { + _server.Parse(new Line(":nickname PART #chan")); + + Assert.AreEqual(0, _server.Users.Count); + Assert.AreEqual(0, _server.Channels.Count); + } + + [TestMethod] + public void PartOther() + { + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":other PART #chan")); + + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + var chanUser = channel.Users[user.NickNameLower]; + + Assert.AreEqual(channel.NameLower, user.Channels.Single()); + CollectionAssert.AreEqual(new Dictionary {{"nickname", user}}, _server.Users); + CollectionAssert.AreEqual(new Dictionary {{"#chan", channel}}, _server.Channels); + CollectionAssert.AreEqual(new Dictionary {{"nickname", chanUser}}, channel.Users); + } + + [TestMethod] + public void KickSelf() + { + _server.Parse(new Line(":nickname KICK #chan nickname")); + + Assert.AreEqual(0, _server.Users.Count); + Assert.AreEqual(0, _server.Channels.Count); + } + + [TestMethod] + public void KickOther() + { + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":nickname KICK #chan other")); + + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + var chanUser = channel.Users[user.NickNameLower]; + + Assert.AreEqual(1, _server.Users.Count); + Assert.AreEqual(1, _server.Channels.Count); + Assert.AreEqual(channel.NameLower, user.Channels.Single()); + CollectionAssert.AreEqual(new Dictionary {{user.NickNameLower, chanUser}}, + channel.Users); + } + + [TestMethod] + public void QuitSelf() + { + _server.Parse(new Line("QUIT :i'm outta here")); + Assert.IsFalse(_server.Users.Any()); + Assert.IsFalse(_server.Channels.Any()); + } + + [TestMethod] + public void QuitSelfWithSource() + { + _server.Parse(new Line(":nickname QUIT :i'm outta here")); + Assert.IsFalse(_server.Users.Any()); + Assert.IsFalse(_server.Channels.Any()); + } + + [TestMethod] + public void QuitOther() + { + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":other QUIT :see ya")); + Assert.IsFalse(_server.Users.ContainsKey("other")); + } + + [TestMethod] + public void TopicText() + { + _server.Parse(new Line("332 * #chan :test")); + Assert.AreEqual("test", _server.Channels["#chan"].Topic); + } + + [TestMethod] + public void TopicSetByAt() + { + var dt = DateTimeOffset.FromUnixTimeSeconds(1584023277).DateTime; + _server.Parse(new Line("333 * #chan other 1584023277")); + + var channel = _server.Channels["#chan"]; + + Assert.AreEqual("other", channel.TopicSetter); + Assert.AreEqual(dt, channel.TopicTime); + } + + [TestMethod] + public void TopicCommand() + { + _server.Parse(new Line("TOPIC #chan :hello there")); + Assert.AreEqual("hello there", _server.Channels["#chan"].Topic); + } + + [TestMethod] + public void CreationDate() + { + _server.Parse(new Line("329 * #chan 1584041889")); + Assert.AreEqual(DateTimeOffset.FromUnixTimeSeconds(1584041889).DateTime, _server.Channels["#chan"].Created); + } + + [TestMethod] + public void NamesCommand() + { + _server.Parse(new Line("353 * * #chan :nickname @+other")); + Assert.IsTrue(_server.Users.ContainsKey("nickname")); + Assert.IsTrue(_server.Users.ContainsKey("other")); + + var user = _server.Users["other"]; + var channel = _server.Channels["#chan"]; + var chanUser1 = channel.Users[user.NickNameLower]; + var chanUser2 = channel.Users[_server.NickNameLower]; + + Assert.AreEqual(2, channel.Users.Count); + CollectionAssert.AreEqual(chanUser1.Modes, channel.Users[user.NickNameLower].Modes); + CollectionAssert.AreEqual(chanUser2.Modes, channel.Users[_server.NickNameLower].Modes); + CollectionAssert.AreEqual(new List {"o", "v"}, chanUser1.Modes); + Assert.AreEqual(channel.NameLower, user.Channels.Single()); + } + + [TestMethod] + public void UserhostInNames() + { + _server.Parse(new Line("353 * * #chan :nickname!user@host other!user2@host2")); + Assert.AreEqual("user", _server.UserName); + Assert.AreEqual("host", _server.HostName); + + var user = _server.Users["other"]; + Assert.AreEqual("user2", user.UserName); + Assert.AreEqual("host2", user.HostName); + } + + [TestMethod] + public void NickAfterJoin() + { + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + var chanUser = channel.Users[user.NickNameLower]; + _server.Parse(new Line(":nickname NICK nickname2")); + CollectionAssert.AreEqual(new Dictionary {{user.NickNameLower, chanUser}}, + channel.Users); + } + } +} diff --git a/IRCStates/Tests/Emit.cs b/IRCStates/Tests/Emit.cs new file mode 100644 index 0000000..07fea8c --- /dev/null +++ b/IRCStates/Tests/Emit.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Emit + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + } + + [TestMethod] + public void EmitJoin() + { + var emit = _server.Parse(new Line(":nickname JOIN #chan")); + + Assert.AreEqual("JOIN", emit.Command); + Assert.IsTrue(emit.Self); + Assert.AreEqual(_server.Users["nickname"], emit.User); + Assert.AreEqual(_server.Channels["#chan"], emit.Channel); + + emit = _server.Parse(new Line(":other JOIN #chan")); + Assert.IsNotNull(emit); + Assert.AreEqual("JOIN", emit.Command); + Assert.IsFalse(emit.Self); + Assert.AreEqual(_server.Users["other"], emit.User); + Assert.AreEqual(_server.Channels["#chan"], emit.Channel); + } + + [TestMethod] + public void EmitPrivmsg() + { + _server.Parse(new Line(":nickname JOIN #chan")); + var emit = _server.Parse(new Line(":nickname PRIVMSG #chan :hello")); + + Assert.IsNotNull(emit); + Assert.AreEqual("PRIVMSG", emit.Command); + Assert.AreEqual("hello", emit.Text); + Assert.IsTrue(emit.SelfSource); + Assert.AreEqual(_server.Users["nickname"], emit.User); + Assert.AreEqual(_server.Channels["#chan"], emit.Channel); + + _server.Parse(new Line(":other JOIN #chan")); + emit = _server.Parse(new Line(":other PRIVMSG #chan :hello2")); + + Assert.IsNotNull(emit); + Assert.AreEqual("PRIVMSG", emit.Command); + Assert.AreEqual("hello2", emit.Text); + Assert.IsFalse(emit.SelfSource); + Assert.AreEqual(_server.Users["other"], emit.User); + Assert.AreEqual(_server.Channels["#chan"], emit.Channel); + } + + [TestMethod] + public void EmitPrivmsgNoJoin() + { + _server.Parse(new Line(":nickname JOIN #chan")); + var emit = _server.Parse(new Line(":other PRIVMSG #chan :hello")); + + Assert.IsNotNull(emit); + Assert.AreEqual("PRIVMSG", emit.Command); + Assert.AreEqual("hello", emit.Text); + Assert.IsFalse(emit.SelfSource); + Assert.IsNotNull(emit.User); + + var channel = _server.Channels["#chan"]; + Assert.AreEqual(channel, emit.Channel); + } + + [TestMethod] + public void EmitKick() + { + _server.Parse(new Line(":nickname JOIN #chan")); + + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + _server.Parse(new Line(":other JOIN #chan")); + var userOther = _server.Users["other"]; + var emit = _server.Parse(new Line(":nickname KICK #chan other :reason")); + + Assert.IsNotNull(emit); + Assert.AreEqual("KICK", emit.Command); + Assert.AreEqual("reason", emit.Text); + Assert.IsTrue(emit.SelfSource); + Assert.AreEqual(user, emit.UserSource); + Assert.AreEqual(userOther, emit.UserTarget); + Assert.AreEqual(channel, emit.Channel); + } + + [TestMethod] + public void EmitMode() + { + var emit = _server.Parse(new Line("MODE nickname x+i-i+wi-wi")); + + Assert.IsNotNull(emit); + Assert.AreEqual("MODE", emit.Command); + Assert.IsTrue(emit.SelfTarget); + CollectionAssert.AreEqual(new List + { + "+x", + "+i", + "-i", + "+w", + "+i", + "-w", + "-i" + }, emit.Tokens); + } + } +} diff --git a/IRCStates/Tests/ISupport.cs b/IRCStates/Tests/ISupport.cs new file mode 100644 index 0000000..5cdcc61 --- /dev/null +++ b/IRCStates/Tests/ISupport.cs @@ -0,0 +1,210 @@ +using System.Collections.Generic; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +// ReSharper disable InconsistentNaming + +namespace IRCStates.Tests +{ + [TestClass] + public class ISupport + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + } + + [TestMethod] + public void ChanModes() + { + CollectionAssert.AreEqual(new List {"b"}, _server.ISupport.ChanModes.ListModes); + CollectionAssert.AreEqual(new List {"k"}, _server.ISupport.ChanModes.SettingBModes); + CollectionAssert.AreEqual(new List {"l"}, _server.ISupport.ChanModes.SettingCModes); + CollectionAssert.AreEqual(new List + { + "i", + "m", + "n", + "p", + "s", + "t" + }, _server.ISupport.ChanModes.SettingDModes); + + _server.Parse(new Line("005 * CHANMODES=a,b,c,d *")); + + CollectionAssert.AreEqual(new List {"a"}, _server.ISupport.ChanModes.ListModes); + CollectionAssert.AreEqual(new List {"b"}, _server.ISupport.ChanModes.SettingBModes); + CollectionAssert.AreEqual(new List {"c"}, _server.ISupport.ChanModes.SettingCModes); + CollectionAssert.AreEqual(new List {"d"}, _server.ISupport.ChanModes.SettingDModes); + } + + [TestMethod] + public void Prefix() + { + CollectionAssert.AreEqual(new List {"o", "v"}, _server.ISupport.Prefix.Modes); + CollectionAssert.AreEqual(new List {"@", "+"}, _server.ISupport.Prefix.Prefixes); + + Assert.AreEqual("@", _server.ISupport.Prefix.FromMode("o")); + Assert.IsNull(_server.ISupport.Prefix.FromMode("a")); + Assert.AreEqual("o", _server.ISupport.Prefix.FromPrefix("@")); + Assert.IsNull(_server.ISupport.Prefix.FromPrefix("&")); + + _server.Parse(new Line("005 * PREFIX=(qaohv)~&@%+ *")); + CollectionAssert.AreEqual(new List + { + "q", + "a", + "o", + "h", + "v" + }, _server.ISupport.Prefix.Modes); + CollectionAssert.AreEqual(new List + { + "~", + "&", + "@", + "%", + "+" + }, _server.ISupport.Prefix.Prefixes); + Assert.AreEqual("&", _server.ISupport.Prefix.FromMode("a")); + Assert.AreEqual("a", _server.ISupport.Prefix.FromPrefix("&")); + } + + [TestMethod] + public void ChanTypes() + { + CollectionAssert.AreEqual(new List {"#"}, _server.ISupport.ChanTypes); + _server.Parse(new Line("005 * CHANTYPES=#& *")); + CollectionAssert.AreEqual(new List {"#", "&"}, _server.ISupport.ChanTypes); + } + + [TestMethod] + public void Modes() + { + Assert.AreEqual(3, _server.ISupport.Modes); + + _server.Parse(new Line("005 * MODES *")); + Assert.AreEqual(-1, _server.ISupport.Modes); + + _server.Parse(new Line("005 * MODES=5 *")); + Assert.AreEqual(5, _server.ISupport.Modes); + } + + [TestMethod] + public void Rfc1459() + { + Assert.AreEqual(IRCStates.Casemap.CaseMapping.Rfc1459, _server.ISupport.CaseMapping); + _server.Parse(new Line("005 * CASEMAPPING=rfc1459 *")); + Assert.AreEqual(IRCStates.Casemap.CaseMapping.Rfc1459, _server.ISupport.CaseMapping); + var lower = _server.CaseFold(@"ÀTEST[]~\"); + Assert.AreEqual("Àtest{}^|", lower); + } + + [TestMethod] + public void Ascii() + { + _server.Parse(new Line("005 * CASEMAPPING=ascii *")); + Assert.AreEqual(IRCStates.Casemap.CaseMapping.Ascii, _server.ISupport.CaseMapping); + var lower = _server.CaseFold(@"ÀTEST[]~\"); + Assert.AreEqual(@"Àtest[]~\", lower); + } + + [TestMethod] + public void FallbackToRfc1459() + { + _server.Parse(new Line("005 * CASEMAPPING=nonexistent *")); + Assert.AreEqual(IRCStates.Casemap.CaseMapping.Rfc1459, _server.ISupport.CaseMapping); + var lower = _server.CaseFold(@"ÀTEST[]~\"); + Assert.AreEqual("Àtest{}^|", lower); + } + + [TestMethod] + public void Network() + { + Assert.IsNull(_server.ISupport.Network); + _server.Parse(new Line("005 * NETWORK=testnet *")); + Assert.AreEqual("testnet", _server.ISupport.Network); + } + + [TestMethod] + public void StatusMsg() + { + CollectionAssert.AreEqual(new List(), _server.ISupport.StatusMsg); + _server.Parse(new Line("005 * STATUSMSG=&@ *")); + CollectionAssert.AreEqual(new List {"&", "@"}, _server.ISupport.StatusMsg); + } + + [TestMethod] + public void CallerId() + { + Assert.IsNull(_server.ISupport.CallerId); + + _server.Parse(new Line("005 * CALLERID=U *")); + Assert.AreEqual("U", _server.ISupport.CallerId); + + _server.Parse(new Line("005 * CALLERID *")); + Assert.AreEqual("g", _server.ISupport.CallerId); + } + + [TestMethod] + public void Excepts() + { + Assert.IsNull(_server.ISupport.Excepts); + + _server.Parse(new Line("005 * EXCEPTS=U *")); + Assert.AreEqual("U", _server.ISupport.Excepts); + + _server.Parse(new Line("005 * EXCEPTS *")); + Assert.AreEqual("e", _server.ISupport.Excepts); + } + + [TestMethod] + public void Invex() + { + Assert.IsNull(_server.ISupport.Invex); + + _server.Parse(new Line("005 * INVEX=U *")); + Assert.AreEqual("U", _server.ISupport.Invex); + + _server.Parse(new Line("005 * INVEX *")); + Assert.AreEqual("I", _server.ISupport.Invex); + } + + [TestMethod] + public void Whox() + { + Assert.IsFalse(_server.ISupport.Whox); + + _server.Parse(new Line("005 * WHOX *")); + Assert.IsTrue(_server.ISupport.Whox); + } + + [TestMethod] + public void Monitor() + { + Assert.IsNull(_server.ISupport.Monitor); + + _server.Parse(new Line("005 * MONITOR=123 *")); + Assert.AreEqual(123, _server.ISupport.Monitor); + + _server.Parse(new Line("005 * MONITOR *")); + Assert.AreEqual(-1, _server.ISupport.Monitor); + } + + [TestMethod] + public void Watch() + { + Assert.IsNull(_server.ISupport.Watch); + + _server.Parse(new Line("005 * WATCH=123 *")); + Assert.AreEqual(123, _server.ISupport.Watch); + + _server.Parse(new Line("005 * WATCH *")); + Assert.AreEqual(-1, _server.ISupport.Watch); + } + } +} diff --git a/IRCStates/Tests/Mode.cs b/IRCStates/Tests/Mode.cs new file mode 100644 index 0000000..90763fa --- /dev/null +++ b/IRCStates/Tests/Mode.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Mode + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + } + + [TestMethod] + public void UModeAdd() + { + _server.Parse(new Line("MODE nickname +i")); + CollectionAssert.AreEqual(new List {"i"}, _server.Modes); + } + + [TestMethod] + public void UModeRemove() + { + _server.Parse(new Line("MODE nickname +i")); + _server.Parse(new Line("MODE nickname -i")); + CollectionAssert.AreEqual(new List(), _server.Modes); + } + + [TestMethod] + public void PrefixAdd() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +ov nickname nickname")); + + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + var channelUser = channel.Users[user.NickNameLower]; + CollectionAssert.AreEqual(new List {"o", "v"}, channelUser.Modes); + } + + [TestMethod] + public void PrefixRemove() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +ov nickname nickname")); + _server.Parse(new Line("MODE #chan -ov nickname nickname")); + + var user = _server.Users["nickname"]; + var channel = _server.Channels["#chan"]; + var channelUser = channel.Users[user.NickNameLower]; + CollectionAssert.AreEqual(new List(), channelUser.Modes); + } + + [TestMethod] + public void ChannelListAdd() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +b asd!*@*")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new List {"asd!*@*"}, channel.ListModes["b"]); + } + + [TestMethod] + public void ChannelListRemove() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +b asd!*@*")); + _server.Parse(new Line("MODE #chan -b asd!*@*")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary>(), channel.ListModes); + } + + [TestMethod] + public void ChannelTypeBAdd() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +k password")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary {{"k", "password"}}, channel.Modes); + } + + [TestMethod] + public void ChannelTypeBRemove() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +k password")); + _server.Parse(new Line("MODE #chan -k *")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary(), channel.Modes); + } + + [TestMethod] + public void ChannelTypeCAdd() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +l 100")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary {{"l", "100"}}, channel.Modes); + } + + [TestMethod] + public void ChannelTypeCRemove() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +l 100")); + _server.Parse(new Line("MODE #chan -l")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary(), channel.Modes); + } + + [TestMethod] + public void ChannelTypeDAdd() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +i")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary {{"i", null}}, channel.Modes); + } + + [TestMethod] + public void ChannelTypeDRemove() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("MODE #chan +i")); + _server.Parse(new Line("MODE #chan -i")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary(), channel.Modes); + } + + [TestMethod] + public void ChannelNumeric() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("324 * #chan +bkli *!*@* pass 10")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary {{"k", "pass"}, {"l", "10"}, {"i", null}}, + channel.Modes); + CollectionAssert.AreEqual(new List {"*!*@*"}, channel.ListModes["b"]); + } + + [TestMethod] + public void ChannelNumericWithoutPlus() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("324 * #chan il 10")); + + var channel = _server.Channels["#chan"]; + CollectionAssert.AreEqual(new Dictionary {{"i", null}, {"l", "10"}}, channel.Modes); + } + + [TestMethod] + public void UserNumeric() + { + _server.Parse(new Line("221 * +iw")); + CollectionAssert.AreEqual(new List {"i", "w"}, _server.Modes); + } + + [TestMethod] + public void UserNumericWithoutPlus() + { + _server.Parse(new Line("221 * iw")); + CollectionAssert.AreEqual(new List {"i", "w"}, _server.Modes); + } + } +} diff --git a/IRCStates/Tests/Motd.cs b/IRCStates/Tests/Motd.cs new file mode 100644 index 0000000..2d75982 --- /dev/null +++ b/IRCStates/Tests/Motd.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Motd + { + [TestMethod] + public void MessageOfTheDay() + { + var server = new Server("test"); + server.Parse(new Line("001 nickname")); + server.Parse(new Line("375 * :start of motd")); + server.Parse(new Line("372 * :first line of motd")); + server.Parse(new Line("372 * :second line of motd")); + + CollectionAssert.AreEqual(new List {"start of motd", "first line of motd", "second line of motd"}, + server.Motd); + } + } +} diff --git a/IRCStates/Tests/Sasl.cs b/IRCStates/Tests/Sasl.cs new file mode 100644 index 0000000..151ccdf --- /dev/null +++ b/IRCStates/Tests/Sasl.cs @@ -0,0 +1,38 @@ +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Sasl + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("900 * nick!user@host account")); + } + + [TestMethod] + public void LoggedIn() + { + Assert.AreEqual("nick", _server.NickName); + Assert.AreEqual("user", _server.UserName); + Assert.AreEqual("host", _server.HostName); + Assert.AreEqual("account", _server.Account); + } + + [TestMethod] + public void LoggedOut() + { + _server.Parse(new Line("901 * nick1!user1@host1")); + + Assert.AreEqual("nick1", _server.NickName); + Assert.AreEqual("user1", _server.UserName); + Assert.AreEqual("host1", _server.HostName); + Assert.IsTrue(string.IsNullOrEmpty(_server.Account)); + } + } +} diff --git a/IRCStates/Tests/User.cs b/IRCStates/Tests/User.cs new file mode 100644 index 0000000..61d7157 --- /dev/null +++ b/IRCStates/Tests/User.cs @@ -0,0 +1,298 @@ +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class User + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + } + + [TestMethod] + public void Welcome() + { + Assert.AreEqual("test", _server.Name); + Assert.AreEqual("nickname", _server.NickName); + } + + [TestMethod] + public void NicknameChange() + { + _server.Parse(new Line(":nickname NICK nickname2")); + Assert.AreEqual("nickname2", _server.NickName); + + _server.Parse(new Line(":nickname2 JOIN #chan")); + _server.Parse(new Line(":other JOIN #chan")); + Assert.IsTrue(_server.Users.ContainsKey("other")); + + _server.Parse(new Line(":other NICK other2")); + Assert.IsFalse(_server.Users.ContainsKey("other")); + Assert.IsTrue(_server.Users.ContainsKey("other2")); + } + + [TestMethod] + public void HostmaskJoinBoth() + { + _server.Parse(new Line(":nickname!user@host JOIN #chan")); + Assert.AreEqual("user", _server.UserName); + Assert.AreEqual("host", _server.HostName); + + _server.Parse(new Line(":other!user@host JOIN #chan")); + var user = _server.Users["other"]; + Assert.AreEqual("user", user.UserName); + Assert.AreEqual("host", user.HostName); + } + + [TestMethod] + public void HostmaskJoinUser() + { + _server.Parse(new Line(":nickname!user JOIN #chan")); + Assert.AreEqual("user", _server.UserName); + Assert.IsNull(_server.HostName); + + _server.Parse(new Line(":other!user JOIN #chan")); + var user = _server.Users["other"]; + Assert.AreEqual("user", user.UserName); + Assert.IsNull(user.HostName); + } + + [TestMethod] + public void HostmaskJoinHost() + { + _server.Parse(new Line(":nickname@host JOIN #chan")); + Assert.IsNull(_server.UserName); + Assert.AreEqual("host", _server.HostName); + + _server.Parse(new Line(":other@host JOIN #chan")); + var user = _server.Users["other"]; + Assert.IsNull(user.UserName); + Assert.AreEqual("host", user.HostName); + } + + [TestMethod] + public void ExtendedJoinWithoutExtendedJoin() + { + _server.Parse(new Line(":nickname JOIN #chan")); + Assert.IsNull(_server.Account); + Assert.IsNull(_server.RealName); + + _server.Parse(new Line(":other JOIN #chan")); + var user = _server.Users["other"]; + Assert.IsNull(user.Account); + Assert.IsNull(user.RealName); + } + + [TestMethod] + public void ExtendedJoinWithAccount() + { + _server.Parse(new Line(":nickname JOIN #chan acc :realname")); + Assert.AreEqual("acc", _server.Account); + Assert.AreEqual("realname", _server.RealName); + + _server.Parse(new Line(":other JOIN #chan acc2 :realname2")); + var user = _server.Users["other"]; + Assert.AreEqual("acc2", user.Account); + Assert.AreEqual("realname2", user.RealName); + } + + [TestMethod] + public void ExtendedJoinWithoutAccount() + { + _server.Parse(new Line(":nickname JOIN #chan * :realname")); + Assert.AreEqual("", _server.Account); + Assert.AreEqual("realname", _server.RealName); + + _server.Parse(new Line(":other JOIN #chan * :realname2")); + var user = _server.Users["other"]; + Assert.AreEqual("", user.Account); + Assert.AreEqual("realname2", user.RealName); + } + + [TestMethod] + public void AccountNotifyWithAccount() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":nickname ACCOUNT acc")); + Assert.AreEqual("acc", _server.Account); + + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":other ACCOUNT acc2")); + var user = _server.Users["other"]; + Assert.AreEqual("acc2", user.Account); + } + + [TestMethod] + public void AccountNotifyWithoutAccount() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":nickname ACCOUNT *")); + Assert.AreEqual("", _server.Account); + + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":other ACCOUNT *")); + var user = _server.Users["other"]; + Assert.AreEqual("", user.Account); + } + + [TestMethod] + public void HostmaskPrivmsgBoth() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":nickname!user@host PRIVMSG #chan :hi")); + Assert.AreEqual("user", _server.UserName); + Assert.AreEqual("host", _server.HostName); + + _server.Parse(new Line(":other!user@host PRIVMSG #chan :hi")); + var user = _server.Users["other"]; + Assert.AreEqual("user", user.UserName); + Assert.AreEqual("host", user.HostName); + } + + [TestMethod] + public void HostmaskPrivmsgUser() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":nickname!user PRIVMSG #chan :hi")); + Assert.AreEqual("user", _server.UserName); + Assert.IsNull(_server.HostName); + + _server.Parse(new Line(":other!user PRIVMSG #chan :hi")); + var user = _server.Users["other"]; + Assert.AreEqual("user", user.UserName); + Assert.IsNull(user.HostName); + } + + [TestMethod] + public void HostmaskPrivmsgHost() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":nickname@host PRIVMSG #chan :hi")); + Assert.IsNull(_server.UserName); + Assert.AreEqual("host", _server.HostName); + + _server.Parse(new Line(":other@host PRIVMSG #chan :hi")); + var user = _server.Users["other"]; + Assert.IsNull(user.UserName); + Assert.AreEqual("host", user.HostName); + } + + [TestMethod] + public void VisibleHostWithoutUsername() + { + _server.Parse(new Line("396 * hostname")); + Assert.IsNull(_server.UserName); + Assert.AreEqual("hostname", _server.HostName); + } + + [TestMethod] + public void VisibleHostWithUsername() + { + _server.Parse(new Line("396 * username@hostname")); + Assert.AreEqual("username", _server.UserName); + Assert.AreEqual("hostname", _server.HostName); + } + + [TestMethod] + public void Who() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line("352 * #chan user host * nickname * :0 real")); + _server.Parse(new Line("352 * #chan user2 host2 * other * :0 real2")); + + Assert.AreEqual("user", _server.UserName); + Assert.AreEqual("host", _server.HostName); + Assert.AreEqual("real", _server.RealName); + + var user = _server.Users["other"]; + Assert.AreEqual("user2", user.UserName); + Assert.AreEqual("host2", user.HostName); + Assert.AreEqual("real2", user.RealName); + } + + [TestMethod] + public void Chghost() + { + _server.Parse(new Line(":nickname!user@host JOIN #chan")); + _server.Parse(new Line(":nickname CHGHOST u h")); + Assert.AreEqual("u", _server.UserName); + Assert.AreEqual("h", _server.HostName); + + _server.Parse(new Line(":other!user2@host2 JOIN #chan")); + _server.Parse(new Line(":other CHGHOST u2 h2")); + var user = _server.Users["other"]; + Assert.AreEqual("u2", user.UserName); + Assert.AreEqual("h2", user.HostName); + } + + [TestMethod] + public void Whois() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line("311 * nickname u h * :r")); + Assert.AreEqual("u", _server.UserName); + Assert.AreEqual("h", _server.HostName); + Assert.AreEqual("r", _server.RealName); + + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":other CHGHOST u2 h2")); + _server.Parse(new Line("311 * other u2 h2 * :r2")); + var user = _server.Users["other"]; + Assert.AreEqual("u2", user.UserName); + Assert.AreEqual("h2", user.HostName); + Assert.AreEqual("r2", user.RealName); + } + + [TestMethod] + public void AwaySet() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":other JOIN #chan")); + var user = _server.Users["other"]; + Assert.IsNull(_server.Away); + Assert.IsNull(user.Away); + + _server.Parse(new Line(":nickname AWAY :bye bye")); + _server.Parse(new Line(":other AWAY :ich geh weg")); + Assert.AreEqual("bye bye", _server.Away); + Assert.AreEqual("ich geh weg", user.Away); + } + + [TestMethod] + public void AwayUnset() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":other JOIN #chan")); + _server.Parse(new Line(":nickname AWAY :bye bye")); + _server.Parse(new Line(":nickname AWAY")); + _server.Parse(new Line(":other AWAY :ich geh weg")); + _server.Parse(new Line(":other AWAY")); + + var user = _server.Users["other"]; + Assert.IsNull(_server.Away); + Assert.IsNull(user.Away); + } + + [TestMethod] + public void Setname() + { + _server.Parse(new Line(":nickname JOIN #chan")); + _server.Parse(new Line(":other JOIN #chan")); + var user = _server.Users["other"]; + Assert.IsNull(user.RealName); + Assert.IsNull(_server.RealName); + + _server.Parse(new Line(":nickname SETNAME :new now know how")); + _server.Parse(new Line(":other SETNAME :tyrannosaurus hex")); + Assert.AreEqual("new now know how", _server.RealName); + Assert.AreEqual("tyrannosaurus hex", user.RealName); + } + } +} diff --git a/IRCStates/Tests/Who.cs b/IRCStates/Tests/Who.cs new file mode 100644 index 0000000..d091785 --- /dev/null +++ b/IRCStates/Tests/Who.cs @@ -0,0 +1,61 @@ +using IRCTokens; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCStates.Tests +{ + [TestClass] + public class Who + { + private Server _server; + + [TestInitialize] + public void TestInitialize() + { + _server = new Server("test"); + _server.Parse(new Line("001 nickname")); + _server.Parse(new Line(":nickname JOIN #chan")); + } + + [TestMethod] + public void WhoResponse() + { + _server.Parse(new Line("352 * #chan user host server nickname * :0 real")); + var user = _server.Users["nickname"]; + + Assert.AreEqual("user", user.UserName); + Assert.AreEqual("host", _server.HostName); + Assert.AreEqual("real", user.RealName); + + Assert.AreEqual(user.UserName, _server.UserName); + Assert.AreEqual(user.HostName, _server.HostName); + Assert.AreEqual(user.RealName, _server.RealName); + } + + [TestMethod] + public void Whox() + { + _server.Parse(new Line($"354 * {Server.WhoType} user realip host nickname account :real")); + var user = _server.Users["nickname"]; + + Assert.AreEqual("user", user.UserName); + Assert.AreEqual("host", user.HostName); + Assert.AreEqual("real", user.RealName); + Assert.AreEqual("account", user.Account); + + Assert.AreEqual(user.UserName, _server.UserName); + Assert.AreEqual(user.HostName, _server.HostName); + Assert.AreEqual(user.RealName, _server.RealName); + Assert.AreEqual(user.Account, _server.Account); + } + + [TestMethod] + public void WhoxNoAccount() + { + _server.Parse(new Line($"354 * {Server.WhoType} user realip host nickname 0 :real")); + var user = _server.Users["nickname"]; + + Assert.IsNull(user.Account); + Assert.AreEqual(user.Account, _server.Account); + } + } +} diff --git a/IRCStates/User.cs b/IRCStates/User.cs new file mode 100644 index 0000000..5e18443 --- /dev/null +++ b/IRCStates/User.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace IRCStates +{ + public class User + { + public User() + { + Channels = new HashSet(); + } + + 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 HashSet Channels { get; set; } + + public override string ToString() + { + return $"User(nickname={NickName})"; + } + + public void SetNickName(string nick, string nickLower) + { + NickName = nick; + NickNameLower = nickLower; + } + } +} -- cgit 1.4.1