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 --- Examples/States/Client.cs | 4 +- Examples/States/StatesSample.csproj | 2 +- Examples/Tokens/Client.cs | 2 +- Examples/Tokens/Program.cs | 2 +- Examples/Tokens/TokensSample.csproj | 2 +- IRCSharp.sln | 48 ++ 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 ++ IRCTokens/Extensions.cs | 62 ++ IRCTokens/Hostmask.cs | 64 +++ IRCTokens/IRCTokens.csproj | 29 + IRCTokens/Line.cs | 233 ++++++++ IRCTokens/README.md | 96 ++++ IRCTokens/StatefulDecoder.cs | 79 +++ IRCTokens/StatefulEncoder.cs | 71 +++ IRCTokens/Tests/Data/JoinModel.cs | 30 + IRCTokens/Tests/Data/SplitModel.cs | 15 + IRCTokens/Tests/Data/msg-join.yaml | 221 ++++++++ IRCTokens/Tests/Data/msg-split.yaml | 343 +++++++++++ IRCTokens/Tests/Format.cs | 105 ++++ IRCTokens/Tests/Hostmask.cs | 64 +++ IRCTokens/Tests/Parser.cs | 55 ++ IRCTokens/Tests/StatefulDecoder.cs | 88 +++ IRCTokens/Tests/StatefulEncoder.cs | 84 +++ IRCTokens/Tests/Tokenization.cs | 118 ++++ IrcSharp.sln | 48 -- IrcStates/Casemap.cs | 38 -- IrcStates/Channel.cs | 67 --- IrcStates/ChannelUser.cs | 32 -- IrcStates/Commands.cs | 22 - IrcStates/Emit.cs | 24 - IrcStates/Extensions.cs | 40 -- IrcStates/ISupport.cs | 114 ---- IrcStates/ISupportChanModes.cs | 33 -- IrcStates/ISupportPrefix.cs | 44 -- IrcStates/IrcStates.csproj | 21 - 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 -- IrcTokens/Extensions.cs | 62 -- IrcTokens/Hostmask.cs | 64 --- IrcTokens/IrcTokens.csproj | 29 - IrcTokens/Line.cs | 233 -------- IrcTokens/README.md | 96 ---- IrcTokens/StatefulDecoder.cs | 79 --- IrcTokens/StatefulEncoder.cs | 71 --- IrcTokens/Tests/Data/JoinModel.cs | 30 - IrcTokens/Tests/Data/SplitModel.cs | 15 - IrcTokens/Tests/Data/msg-join.yaml | 221 -------- IrcTokens/Tests/Data/msg-split.yaml | 343 ----------- IrcTokens/Tests/Format.cs | 105 ---- IrcTokens/Tests/Hostmask.cs | 64 --- IrcTokens/Tests/Parser.cs | 55 -- IrcTokens/Tests/StatefulDecoder.cs | 88 --- IrcTokens/Tests/StatefulEncoder.cs | 84 --- IrcTokens/Tests/Tokenization.cs | 118 ---- README.md | 2 +- 94 files changed, 4665 insertions(+), 4690 deletions(-) create mode 100644 IRCSharp.sln 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 create mode 100644 IRCTokens/Extensions.cs create mode 100644 IRCTokens/Hostmask.cs create mode 100644 IRCTokens/IRCTokens.csproj create mode 100644 IRCTokens/Line.cs create mode 100644 IRCTokens/README.md create mode 100644 IRCTokens/StatefulDecoder.cs create mode 100644 IRCTokens/StatefulEncoder.cs create mode 100644 IRCTokens/Tests/Data/JoinModel.cs create mode 100644 IRCTokens/Tests/Data/SplitModel.cs create mode 100644 IRCTokens/Tests/Data/msg-join.yaml create mode 100644 IRCTokens/Tests/Data/msg-split.yaml create mode 100644 IRCTokens/Tests/Format.cs create mode 100644 IRCTokens/Tests/Hostmask.cs create mode 100644 IRCTokens/Tests/Parser.cs create mode 100644 IRCTokens/Tests/StatefulDecoder.cs create mode 100644 IRCTokens/Tests/StatefulEncoder.cs create mode 100644 IRCTokens/Tests/Tokenization.cs delete mode 100644 IrcSharp.sln delete mode 100644 IrcStates/Casemap.cs delete mode 100644 IrcStates/Channel.cs delete mode 100644 IrcStates/ChannelUser.cs delete mode 100644 IrcStates/Commands.cs delete mode 100644 IrcStates/Emit.cs delete mode 100644 IrcStates/Extensions.cs delete mode 100644 IrcStates/ISupport.cs delete mode 100644 IrcStates/ISupportChanModes.cs delete mode 100644 IrcStates/ISupportPrefix.cs delete mode 100644 IrcStates/IrcStates.csproj delete mode 100644 IrcStates/Numeric.cs delete mode 100644 IrcStates/README.md delete mode 100644 IrcStates/Server.cs delete mode 100644 IrcStates/ServerDisconnectedException.cs delete mode 100644 IrcStates/ServerException.cs delete mode 100644 IrcStates/Tests/Cap.cs delete mode 100644 IrcStates/Tests/Casemap.cs delete mode 100644 IrcStates/Tests/Channel.cs delete mode 100644 IrcStates/Tests/Emit.cs delete mode 100644 IrcStates/Tests/ISupport.cs delete mode 100644 IrcStates/Tests/Mode.cs delete mode 100644 IrcStates/Tests/Motd.cs delete mode 100644 IrcStates/Tests/Sasl.cs delete mode 100644 IrcStates/Tests/User.cs delete mode 100644 IrcStates/Tests/Who.cs delete mode 100644 IrcStates/User.cs delete mode 100644 IrcTokens/Extensions.cs delete mode 100644 IrcTokens/Hostmask.cs delete mode 100644 IrcTokens/IrcTokens.csproj delete mode 100644 IrcTokens/Line.cs delete mode 100644 IrcTokens/README.md delete mode 100644 IrcTokens/StatefulDecoder.cs delete mode 100644 IrcTokens/StatefulEncoder.cs delete mode 100644 IrcTokens/Tests/Data/JoinModel.cs delete mode 100644 IrcTokens/Tests/Data/SplitModel.cs delete mode 100644 IrcTokens/Tests/Data/msg-join.yaml delete mode 100644 IrcTokens/Tests/Data/msg-split.yaml delete mode 100644 IrcTokens/Tests/Format.cs delete mode 100644 IrcTokens/Tests/Hostmask.cs delete mode 100644 IrcTokens/Tests/Parser.cs delete mode 100644 IrcTokens/Tests/StatefulDecoder.cs delete mode 100644 IrcTokens/Tests/StatefulEncoder.cs delete mode 100644 IrcTokens/Tests/Tokenization.cs diff --git a/Examples/States/Client.cs b/Examples/States/Client.cs index 78d253f..e98457d 100644 --- a/Examples/States/Client.cs +++ b/Examples/States/Client.cs @@ -2,8 +2,8 @@ using System; using System.Linq; using System.Net.Sockets; using System.Threading; -using IrcStates; -using IrcTokens; +using IRCStates; +using IRCTokens; namespace StatesSample { diff --git a/Examples/States/StatesSample.csproj b/Examples/States/StatesSample.csproj index ac7b5b7..75b83cc 100644 --- a/Examples/States/StatesSample.csproj +++ b/Examples/States/StatesSample.csproj @@ -6,7 +6,7 @@ - + diff --git a/Examples/Tokens/Client.cs b/Examples/Tokens/Client.cs index 3dd3933..71d1407 100644 --- a/Examples/Tokens/Client.cs +++ b/Examples/Tokens/Client.cs @@ -1,7 +1,7 @@ using System; using System.Net.Sockets; using System.Threading; -using IrcTokens; +using IRCTokens; namespace TokensSample { diff --git a/Examples/Tokens/Program.cs b/Examples/Tokens/Program.cs index ba57836..e157e31 100644 --- a/Examples/Tokens/Program.cs +++ b/Examples/Tokens/Program.cs @@ -1,5 +1,5 @@ using System; -using IrcTokens; +using IRCTokens; namespace TokensSample { diff --git a/Examples/Tokens/TokensSample.csproj b/Examples/Tokens/TokensSample.csproj index a0e98fd..6dbcdcf 100644 --- a/Examples/Tokens/TokensSample.csproj +++ b/Examples/Tokens/TokensSample.csproj @@ -6,7 +6,7 @@ - + diff --git a/IRCSharp.sln b/IRCSharp.sln new file mode 100644 index 0000000..ff19f5e --- /dev/null +++ b/IRCSharp.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRCTokens", "IRCTokens\IRCTokens.csproj", "{9E812F45-B2CD-42D2-8378-EBEBF8697905}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokensSample", "Examples\Tokens\TokensSample.csproj", "{A45DA39B-6B47-4713-8049-3B36E0235B67}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRCStates", "IRCStates\IRCStates.csproj", "{233E3CB4-61F1-4368-9139-7E9F4A58ED2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatesSample", "Examples\States\StatesSample.csproj", "{BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A85EB22-D7B4-417F-AC3B-DAFD97DDEA08}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.Build.0 = Release|Any CPU + {A45DA39B-6B47-4713-8049-3B36E0235B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A45DA39B-6B47-4713-8049-3B36E0235B67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A45DA39B-6B47-4713-8049-3B36E0235B67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A45DA39B-6B47-4713-8049-3B36E0235B67}.Release|Any CPU.Build.0 = Release|Any CPU + {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Release|Any CPU.Build.0 = Release|Any CPU + {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0B91F0EA-8564-4318-8EEC-ED0640475141} + EndGlobalSection +EndGlobal 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; + } + } +} diff --git a/IRCTokens/Extensions.cs b/IRCTokens/Extensions.cs new file mode 100644 index 0000000..e346a43 --- /dev/null +++ b/IRCTokens/Extensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IRCTokens +{ + public static class Extensions + { + public static IEnumerable Split(this byte[] bytes, byte separator) + { + if (bytes == null || bytes.Length == 0) return new List(); + + var newLineIndices = bytes.Select((b, i) => b == separator ? i : -1).Where(i => i != -1).ToArray(); + var lines = new byte[newLineIndices.Length + 1][]; + var currentIndex = 0; + var arrIndex = 0; + + for (var i = 0; i < newLineIndices.Length && currentIndex < bytes.Length; ++i) + { + var n = new byte[newLineIndices[i] - currentIndex]; + Array.Copy(bytes, currentIndex, n, 0, newLineIndices[i] - currentIndex); + currentIndex = newLineIndices[i] + 1; + lines[arrIndex++] = n; + } + + // Handle the last string at the end of the array if there is one. + if (currentIndex < bytes.Length) + lines[arrIndex] = bytes.Skip(currentIndex).ToArray(); + else if (arrIndex == newLineIndices.Length) + // We had a separator character at the end of a string. Rather than just allowing + // a null character, we'll replace the last element in the array with an empty string. + lines[arrIndex] = Array.Empty(); + + return lines.ToArray(); + } + + public static byte[] Trim(this IEnumerable bytes, byte separator) + { + if (bytes == null) return Array.Empty(); + + var byteList = new List(bytes); + var i = 0; + + if (!byteList.Any()) return byteList.ToArray(); + + while (byteList[i] == separator) + { + byteList.RemoveAt(i); + i++; + } + + i = byteList.Count - 1; + while (byteList[i] == separator) + { + byteList.RemoveAt(i); + i--; + } + + return byteList.ToArray(); + } + } +} diff --git a/IRCTokens/Hostmask.cs b/IRCTokens/Hostmask.cs new file mode 100644 index 0000000..2e1549a --- /dev/null +++ b/IRCTokens/Hostmask.cs @@ -0,0 +1,64 @@ +using System; + +namespace IRCTokens +{ + /// + /// Represents the three parts of a hostmask. Parse with the constructor. + /// + public class Hostmask : IEquatable + { + private readonly string _source; + + public Hostmask(string source) + { + if (source == null) return; + + _source = source; + + if (source.Contains('@', StringComparison.Ordinal)) + { + var split = source.Split('@'); + + NickName = split[0]; + HostName = split[1]; + } + else + { + NickName = source; + } + + if (NickName.Contains('!', StringComparison.Ordinal)) + { + var userSplit = NickName.Split('!'); + NickName = userSplit[0]; + UserName = userSplit[1]; + } + } + + public string NickName { get; set; } + public string UserName { get; set; } + public string HostName { get; set; } + + public bool Equals(Hostmask other) + { + if (other == null) return false; + + return _source == other._source; + } + + public override string ToString() + { + return _source; + } + + public override int GetHashCode() + { + return _source.GetHashCode(StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + return Equals(obj as Hostmask); + } + } +} diff --git a/IRCTokens/IRCTokens.csproj b/IRCTokens/IRCTokens.csproj new file mode 100644 index 0000000..2fe9300 --- /dev/null +++ b/IRCTokens/IRCTokens.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp3.1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/IRCTokens/Line.cs b/IRCTokens/Line.cs new file mode 100644 index 0000000..bf3cc91 --- /dev/null +++ b/IRCTokens/Line.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace IRCTokens +{ + /// + /// Tools to represent, parse, and format IRC lines + /// + public class Line : IEquatable + { + private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"}; + + private static readonly string[] TagEscaped = {"\\\\", "\\s", "\\:", "\\r", "\\n"}; + + private Hostmask _hostmask; + + public Line() + { + } + + public Line(string command, params string[] parameters) + { + Command = command; + Params = parameters.ToList(); + } + + /// + /// Build new object parsed from + /// a string + /// . Analogous to irctokens.tokenise() + /// + /// irc line to parse + public Line(string line) + { + if (string.IsNullOrWhiteSpace(line)) throw new ArgumentNullException(nameof(line)); + + string[] split; + + if (line.StartsWith('@')) + { + Tags = new Dictionary(); + + split = line.Split(" ", 2); + var messageTags = split[0]; + line = split[1]; + + foreach (var part in messageTags.Substring(1).Split(';')) + if (part.Contains('=', StringComparison.Ordinal)) + { + split = part.Split('=', 2); + Tags[split[0]] = UnescapeTag(split[1]); + } + else + { + Tags[part] = string.Empty; + } + } + + string trailing; + if (line.Contains(" :", StringComparison.Ordinal)) + { + split = line.Split(" :", 2); + line = split[0]; + trailing = split[1]; + } + else + { + trailing = null; + } + + Params = line.Contains(' ', StringComparison.Ordinal) + ? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() + : new List {line}; + + if (Params[0].StartsWith(':')) + { + Source = Params[0].Substring(1); + Params.RemoveAt(0); + } + + if (Params.Count > 0) + { + Command = Params[0].ToUpper(CultureInfo.InvariantCulture); + Params.RemoveAt(0); + } + + if (trailing != null) Params.Add(trailing); + } + + public Dictionary Tags { get; set; } + public string Source { get; set; } + public string Command { get; set; } + public List Params { get; set; } + + public Hostmask Hostmask => + _hostmask ??= new Hostmask(Source); + + public bool Equals(Line other) + { + if (other == null) return false; + + return Format() == other.Format(); + } + + /// + /// Unescape ircv3 tag + /// + /// escaped string + /// unescaped string + private static string UnescapeTag(string val) + { + var unescaped = new StringBuilder(); + + var graphemeIterator = StringInfo.GetTextElementEnumerator(val); + graphemeIterator.Reset(); + + while (graphemeIterator.MoveNext()) + { + var current = graphemeIterator.GetTextElement(); + + if (current == @"\") + try + { + graphemeIterator.MoveNext(); + var next = graphemeIterator.GetTextElement(); + var pair = current + next; + unescaped.Append(TagEscaped.Contains(pair) + ? TagUnescaped[Array.IndexOf(TagEscaped, pair)] + : next); + } + catch (InvalidOperationException) + { + // ignored + } + else + unescaped.Append(current); + } + + return unescaped.ToString(); + } + + /// + /// Escape strings for use in ircv3 tags + /// + /// string to escape + /// escaped string + private static string EscapeTag(string val) + { + for (var i = 0; i < TagUnescaped.Length; ++i) + val = val?.Replace(TagUnescaped[i], TagEscaped[i], StringComparison.Ordinal); + + return val; + } + + public override string ToString() + { + var vars = new List(); + + if (Command != null) vars.Add($"command={Command}"); + + if (Source != null) vars.Add($"source={Source}"); + + if (Params != null && Params.Any()) vars.Add($"params=[{string.Join(",", Params)}]"); + + if (Tags != null && Tags.Any()) + vars.Add($"tags=[{string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]"); + + return $"Line({string.Join(", ", vars)})"; + } + + public override int GetHashCode() + { + return Format().GetHashCode(StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + return Equals(obj as Line); + } + + /// + /// Format a as a standards-compliant IRC line + /// + /// formatted irc line + public string Format() + { + var outs = new List(); + + if (Tags != null && Tags.Any()) + { + var tags = Tags.Keys + .OrderBy(k => k) + .Select(key => + string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={EscapeTag(Tags[key])}") + .ToList(); + + outs.Add($"@{string.Join(";", tags)}"); + } + + if (Source != null) outs.Add($":{Source}"); + + outs.Add(Command); + + if (Params != null && Params.Any()) + { + var last = Params[^1]; + var withoutLast = Params.SkipLast(1).ToList(); + + foreach (var p in withoutLast) + { + if (p.Contains(' ', StringComparison.Ordinal)) + throw new ArgumentException(@"non-last parameters cannot have spaces", p); + + if (p.StartsWith(':')) + throw new ArgumentException(@"non-last parameters cannot start with colon", p); + } + + outs.AddRange(withoutLast); + + if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) || + last.StartsWith(':')) + last = $":{last}"; + + outs.Add(last); + } + + return string.Join(" ", outs); + } + } +} diff --git a/IRCTokens/README.md b/IRCTokens/README.md new file mode 100644 index 0000000..d3769aa --- /dev/null +++ b/IRCTokens/README.md @@ -0,0 +1,96 @@ +# IRCTokens + +this is a c\# port of jesopo's [irctokens]( +https://github.com/jesopo/irctokens) + +## usage + +### tokenization + + using IRCTokens; + + ... + + var line = new Line("@id=123 :ben!~ben@host.tld PRIVMSG #channel :hello there!"); + Console.WriteLine(line); + Console.WriteLine(line.Format()); + +### formatting + + var line = new Line {Command = "USER", Params = new List {"user", "0", "*", "real name"}}; + Console.WriteLine(line); + Console.WriteLine(line.Format()); + +### stateful + +see the full example in [TokensSample/Client.cs](../Examples/Tokens/Client.cs) + + public class Client + { + private readonly byte[] _bytes; + private readonly StatefulDecoder _decoder; + private readonly StatefulEncoder _encoder; + private readonly Socket _socket; + + public Client() + { + _decoder = new StatefulDecoder(); + _encoder = new StatefulEncoder(); + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); + _bytes = new byte[1024]; + } + + public void Start() + { + _socket.Connect("127.0.0.1", 6667); + while (!_socket.Connected) Thread.Sleep(1000); + + Send(new Line {Command = "NICK", Params = new List {"tokensbot"}}); + Send(new Line {Command = "USER", Params = new List {"tokensbot", "0", "*", "real name"}}); + + while (true) + { + var bytesReceived = _socket.Receive(_bytes); + + if (bytesReceived == 0) + { + Console.WriteLine("! disconnected"); + _socket.Shutdown(SocketShutdown.Both); + _socket.Close(); + break; + } + + var lines = _decoder.Push(_bytes, bytesReceived); + + foreach (var line in lines) + { + Console.WriteLine($"< {line.Format()}"); + + switch (line.Command) + { + case "PING": + Send(new Line {Command = "PONG", Params = line.Params}); + break; + case "001": + Send(new Line {Command = "JOIN", Params = new List {"#test"}}); + break; + case "PRIVMSG": + Send(new Line + { + Command = "PRIVMSG", Params = new List {line.Params[0], "hello there"} + }); + break; + } + } + } + } + + private void Send(Line line) + { + Console.WriteLine($"> {line.Format()}"); + _encoder.Push(line); + while (_encoder.PendingBytes.Length > 0) + _encoder.Pop(_socket.Send(_encoder.PendingBytes, SocketFlags.None)); + } + } + diff --git a/IRCTokens/StatefulDecoder.cs b/IRCTokens/StatefulDecoder.cs new file mode 100644 index 0000000..82630f6 --- /dev/null +++ b/IRCTokens/StatefulDecoder.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace IRCTokens +{ + public class StatefulDecoder + { + private byte[] _buffer; + private Encoding _encoding; + private Encoding _fallback; + + public StatefulDecoder() + { + Clear(); + } + + public Encoding Encoding + { + get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + set + { + if (value != null) + _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ReplacementFallback); + } + } + + public Encoding Fallback + { + get => _fallback ?? Encoding.GetEncoding(Encoding.GetEncoding("iso-8859-1").CodePage, + EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); + set + { + if (value != null) + _fallback = Encoding.GetEncoding(value.CodePage, EncoderFallback.ReplacementFallback, + DecoderFallback.ReplacementFallback); + } + } + + public string Pending => Encoding.GetString(_buffer); + + public void Clear() + { + _buffer = Array.Empty(); + } + + public List Push(string data) + { + var bytes = Encoding.GetBytes(data); + return Push(bytes, bytes.Length); + } + + public List Push(byte[] data, int bytesReceived) + { + if (data == null) return null; + + _buffer = _buffer == null ? Array.Empty() : _buffer.Concat(data.Take(bytesReceived)).ToArray(); + + var listLines = _buffer.Split((byte) '\n').Select(l => l.Trim((byte) '\r')).ToList(); + _buffer = listLines.LastOrDefault() ?? Array.Empty(); + + var decodeLines = new List(); + foreach (var line in listLines.SkipLast(1).Select(l => l.ToArray())) + try + { + decodeLines.Add(new Line(Encoding.GetString(line))); + } + catch (DecoderFallbackException) + { + decodeLines.Add(new Line(Fallback.GetString(line))); + } + + return decodeLines; + } + } +} diff --git a/IRCTokens/StatefulEncoder.cs b/IRCTokens/StatefulEncoder.cs new file mode 100644 index 0000000..46949dd --- /dev/null +++ b/IRCTokens/StatefulEncoder.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace IRCTokens +{ + public class StatefulEncoder + { + private List _bufferedLines; + private Encoding _encoding; + + public StatefulEncoder() + { + Clear(); + } + + public Encoding Encoding + { + get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + set + { + if (value != null) + _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback, + DecoderFallback.ExceptionFallback); + } + } + + public byte[] PendingBytes { get; private set; } + + public string Pending() + { + try + { + return Encoding.GetString(PendingBytes); + } + catch (DecoderFallbackException e) + { + Console.WriteLine(e); + throw; + } + } + + public void Clear() + { + PendingBytes = Array.Empty(); + _bufferedLines = new List(); + } + + public void Push(Line line) + { + if (line == null) throw new ArgumentNullException(nameof(line)); + + PendingBytes = PendingBytes.Concat(Encoding.GetBytes($"{line.Format()}\r\n")).ToArray(); + _bufferedLines.Add(line); + } + + public List Pop(int byteCount) + { + var sent = PendingBytes.Take(byteCount).Count(c => c == '\n'); + + PendingBytes = PendingBytes.Skip(byteCount).ToArray(); + + var sentLines = _bufferedLines.Take(sent).ToList(); + _bufferedLines = _bufferedLines.Skip(sent).ToList(); + + return sentLines; + } + } +} diff --git a/IRCTokens/Tests/Data/JoinModel.cs b/IRCTokens/Tests/Data/JoinModel.cs new file mode 100644 index 0000000..e54f4cf --- /dev/null +++ b/IRCTokens/Tests/Data/JoinModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using YamlDotNet.Serialization; + +namespace IRCTokens.Tests.Data +{ + public class JoinModel + { + public List Tests { get; set; } + + public class Test + { + [YamlMember(Alias = "desc")] public string Description { get; set; } + + public Atoms Atoms { get; set; } + + public List Matches { get; set; } + } + + public class Atoms + { + public Dictionary Tags { get; set; } + + public string Source { get; set; } + + public string Verb { get; set; } + + public List Params { get; set; } + } + } +} diff --git a/IRCTokens/Tests/Data/SplitModel.cs b/IRCTokens/Tests/Data/SplitModel.cs new file mode 100644 index 0000000..5386326 --- /dev/null +++ b/IRCTokens/Tests/Data/SplitModel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace IRCTokens.Tests.Data +{ + public class SplitModel + { + public List Tests { get; set; } + + public class Test + { + public string Input { get; set; } + public JoinModel.Atoms Atoms { get; set; } + } + } +} diff --git a/IRCTokens/Tests/Data/msg-join.yaml b/IRCTokens/Tests/Data/msg-join.yaml new file mode 100644 index 0000000..d1d7429 --- /dev/null +++ b/IRCTokens/Tests/Data/msg-join.yaml @@ -0,0 +1,221 @@ +# IRC parser tests +# joining atoms into sendable messages + +# Written in 2015 by Daniel Oaks +# +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see +# . + +# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed +# https://github.com/grawity/code/tree/master/lib/tests +# some of the tests here originate from Mozilla's test vectors, which is public domain +# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js +# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here +# https://github.com/SaberUK/ircparser/tree/master/test + +tests: + # the desc string holds a description of the test, if it exists + + # the atoms dict has the keys: + # * tags: tags dict + # tags with no value are an empty string + # * source: source string, without single leading colon + # * verb: verb string + # * params: params split up as a list + # if the params key does not exist, assume it is empty + # if any other keys do no exist, assume they are null + # a key that is null does not exist or is not specified with the + # given input string + + # matches is a list of messages that match + + # simple tests + - desc: Simple test with verb and params. + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "asdf" + matches: + - "foo bar baz asdf" + - "foo bar baz :asdf" + + # with no regular params + - desc: Simple test with source and no params. + atoms: + source: "src" + verb: "AWAY" + matches: + - ":src AWAY" + + - desc: Simple test with source and empty trailing param. + atoms: + source: "src" + verb: "AWAY" + params: + - "" + matches: + - ":src AWAY :" + + # with source + - desc: Simple test with source. + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "asdf" + matches: + - ":coolguy foo bar baz asdf" + - ":coolguy foo bar baz :asdf" + + # with trailing param + - desc: Simple test with trailing param. + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "asdf quux" + matches: + - "foo bar baz :asdf quux" + + - desc: Simple test with empty trailing param. + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "" + matches: + - "foo bar baz :" + + - desc: Simple test with trailing param containing colon. + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - ":asdf" + matches: + - "foo bar baz ::asdf" + + # with source and trailing param + - desc: Test with source and trailing param. + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "asdf quux" + matches: + - ":coolguy foo bar baz :asdf quux" + + - desc: Test with trailing containing beginning+end whitespace. + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - " asdf quux " + matches: + - ":coolguy foo bar baz : asdf quux " + + - desc: Test with trailing containing what looks like another trailing param. + atoms: + source: "coolguy" + verb: "PRIVMSG" + params: + - "bar" + - "lol :) " + matches: + - ":coolguy PRIVMSG bar :lol :) " + + - desc: Simple test with source and empty trailing. + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "" + matches: + - ":coolguy foo bar baz :" + + - desc: Trailing contains only spaces. + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - " " + matches: + - ":coolguy foo bar baz : " + + - desc: Param containing tab (tab is not considered SPACE for message splitting). + atoms: + source: "coolguy" + verb: "foo" + params: + - "b\tar" + - "baz" + matches: + - ":coolguy foo b\tar baz" + - ":coolguy foo b\tar :baz" + + # with tags + - desc: Tag with no value and space-filled trailing. + atoms: + tags: + "asd": "" + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - " " + matches: + - "@asd :coolguy foo bar baz : " + + - desc: Tags with escaped values. + atoms: + verb: "foo" + tags: + "a": "b\\and\nk" + "d": "gh;764" + matches: + - "@a=b\\\\and\\nk;d=gh\\:764 foo" + - "@d=gh\\:764;a=b\\\\and\\nk foo" + + - desc: Tags with escaped values and params. + atoms: + verb: "foo" + tags: + "a": "b\\and\nk" + "d": "gh;764" + params: + - "par1" + - "par2" + matches: + - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" + - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" + - "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" + - "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2" + + - desc: Tag with long, strange values (including LF and newline). + atoms: + tags: + foo: "\\\\;\\s \r\n" + verb: "COMMAND" + matches: + - "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" diff --git a/IRCTokens/Tests/Data/msg-split.yaml b/IRCTokens/Tests/Data/msg-split.yaml new file mode 100644 index 0000000..fa3f4aa --- /dev/null +++ b/IRCTokens/Tests/Data/msg-split.yaml @@ -0,0 +1,343 @@ +# IRC parser tests +# splitting messages into usable atoms + +# Written in 2015 by Daniel Oaks +# +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see +# . + +# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed +# https://github.com/grawity/code/tree/master/lib/tests +# some of the tests here originate from Mozilla's test vectors, which is public domain +# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js +# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here +# https://github.com/SaberUK/ircparser/tree/master/test + +# we follow RFC1459 with regards to multiple ascii spaces splitting atoms: +# The prefix, command, and all parameters are +# separated by one (or more) ASCII space character(s) (0x20). +# because doing it as RFC2812 says (strictly as a single ascii space) isn't sane + +tests: + # input is the string coming directly from the server to parse + + # the atoms dict has the keys: + # * tags: tags dict + # tags with no value are an empty string + # * source: source string, without single leading colon + # * verb: verb string + # * params: params split up as a list + # if the params key does not exist, assume it is empty + # if any other keys do no exist, assume they are null + # a key that is null does not exist or is not specified with the + # given input string + + # simple + - input: "foo bar baz asdf" + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "asdf" + + # with source + - input: ":coolguy foo bar baz asdf" + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "asdf" + + # with trailing param + - input: "foo bar baz :asdf quux" + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "asdf quux" + + - input: "foo bar baz :" + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - "" + + - input: "foo bar baz ::asdf" + atoms: + verb: "foo" + params: + - "bar" + - "baz" + - ":asdf" + + # with source and trailing param + - input: ":coolguy foo bar baz :asdf quux" + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "asdf quux" + + - input: ":coolguy foo bar baz : asdf quux " + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - " asdf quux " + + - input: ":coolguy PRIVMSG bar :lol :) " + atoms: + source: "coolguy" + verb: "PRIVMSG" + params: + - "bar" + - "lol :) " + + - input: ":coolguy foo bar baz :" + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - "" + + - input: ":coolguy foo bar baz : " + atoms: + source: "coolguy" + verb: "foo" + params: + - "bar" + - "baz" + - " " + + # with tags + - input: "@a=b;c=32;k;rt=ql7 foo" + atoms: + verb: "foo" + tags: + "a": "b" + "c": "32" + "k": "" + "rt": "ql7" + + # with escaped tags + - input: "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo" + atoms: + verb: "foo" + tags: + "a": "b\\and\nk" + "c": "72 45" + "d": "gh;764" + + # with tags and source + - input: "@c;h=;a=b :quux ab cd" + atoms: + tags: + "c": "" + "h": "" + "a": "b" + source: "quux" + verb: "ab" + params: + - "cd" + + # different forms of last param + - input: ":src JOIN #chan" + atoms: + source: "src" + verb: "JOIN" + params: + - "#chan" + + - input: ":src JOIN :#chan" + atoms: + source: "src" + verb: "JOIN" + params: + - "#chan" + + # with and without last param + - input: ":src AWAY" + atoms: + source: "src" + verb: "AWAY" + + - input: ":src AWAY " + atoms: + source: "src" + verb: "AWAY" + + # tab is not considered + - input: ":cool\tguy foo bar baz" + atoms: + source: "cool\tguy" + verb: "foo" + params: + - "bar" + - "baz" + + # with weird control codes in the source + - input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz" + atoms: + source: "coolguy!ag@net\x035w\x03ork.admin" + verb: "PRIVMSG" + params: + - "foo" + - "bar baz" + + - input: ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz" + atoms: + source: "coolguy!~ag@n\x02et\x0305w\x0fork.admin" + verb: "PRIVMSG" + params: + - "foo" + - "bar baz" + + - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3" + atoms: + tags: + tag1: "value1" + tag2: "" + vendor1/tag3: "value2" + vendor2/tag4: "" + source: "irc.example.com" + verb: "COMMAND" + params: + - "param1" + - "param2" + - "param3 param3" + + - input: ":irc.example.com COMMAND param1 param2 :param3 param3" + atoms: + source: "irc.example.com" + verb: "COMMAND" + params: + - "param1" + - "param2" + - "param3 param3" + + - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3" + atoms: + tags: + tag1: "value1" + tag2: "" + vendor1/tag3: "value2" + vendor2/tag4: "" + verb: "COMMAND" + params: + - "param1" + - "param2" + - "param3 param3" + + - input: "COMMAND" + atoms: + verb: "COMMAND" + + # yaml encoding + slashes is fun + - input: "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" + atoms: + tags: + foo: "\\\\;\\s \r\n" + verb: "COMMAND" + + # broken messages from unreal + - input: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters" + atoms: + source: "gravel.mozilla.org" + verb: "432" + params: + - "#momo" + - "Erroneous Nickname: Illegal characters" + + - input: ":gravel.mozilla.org MODE #tckk +n " + atoms: + source: "gravel.mozilla.org" + verb: "MODE" + params: + - "#tckk" + - "+n" + + - input: ":services.esper.net MODE #foo-bar +o foobar " + atoms: + source: "services.esper.net" + verb: "MODE" + params: + - "#foo-bar" + - "+o" + - "foobar" + + # tag values should be parsed char-at-a-time to prevent wayward replacements. + - input: "@tag1=value\\\\ntest COMMAND" + atoms: + tags: + tag1: "value\\ntest" + verb: "COMMAND" + + # If a tag value has a slash followed by a character which doesn't need + # to be escaped, the slash should be dropped. + - input: "@tag1=value\\1 COMMAND" + atoms: + tags: + tag1: "value1" + verb: "COMMAND" + + # A slash at the end of a tag value should be dropped + - input: "@tag1=value1\\ COMMAND" + atoms: + tags: + tag1: "value1" + verb: "COMMAND" + + # Duplicate tags: Parsers SHOULD disregard all but the final occurence + - input: "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND" + atoms: + tags: + tag1: "5" + tag2: "3" + tag3: "4" + verb: "COMMAND" + + # vendored tags can have the same name as a non-vendored tag + - input: "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND" + atoms: + tags: + tag1: "5" + tag2: "3" + tag3: "4" + vendor/tag2: "8" + verb: "COMMAND" + + # Some parsers handle /MODE in a special way, make sure they do it right + - input: ":SomeOp MODE #channel :+i" + atoms: + source: "SomeOp" + verb: "MODE" + params: + - "#channel" + - "+i" + + - input: ":SomeOp MODE #channel +oo SomeUser :AnotherUser" + atoms: + source: "SomeOp" + verb: "MODE" + params: + - "#channel" + - "+oo" + - "SomeUser" + - "AnotherUser" diff --git a/IRCTokens/Tests/Format.cs b/IRCTokens/Tests/Format.cs new file mode 100644 index 0000000..8319069 --- /dev/null +++ b/IRCTokens/Tests/Format.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCTokens.Tests +{ + [TestClass] + public class Format + { + [TestMethod] + public void TestTags() + { + var line = new Line("PRIVMSG", "#channel", "hello") + { + Tags = new Dictionary {{"id", "\\" + " " + ";" + "\r\n"}} + }.Format(); + + Assert.AreEqual("@id=\\\\\\s\\:\\r\\n PRIVMSG #channel hello", line); + } + + [TestMethod] + public void TestMissingTag() + { + var line = new Line("PRIVMSG", "#channel", "hello").Format(); + + Assert.AreEqual("PRIVMSG #channel hello", line); + } + + [TestMethod] + public void TestNullTag() + { + var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary {{"a", null}}} + .Format(); + + Assert.AreEqual("@a PRIVMSG #channel hello", line); + } + + [TestMethod] + public void TestEmptyTag() + { + var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary {{"a", ""}}} + .Format(); + + Assert.AreEqual("@a PRIVMSG #channel hello", line); + } + + [TestMethod] + public void TestSource() + { + var line = new Line("PRIVMSG", "#channel", "hello") {Source = "nick!user@host"}.Format(); + + Assert.AreEqual(":nick!user@host PRIVMSG #channel hello", line); + } + + [TestMethod] + public void TestCommandLowercase() + { + var line = new Line {Command = "privmsg"}.Format(); + Assert.AreEqual("privmsg", line); + } + + [TestMethod] + public void TestCommandUppercase() + { + var line = new Line {Command = "PRIVMSG"}.Format(); + Assert.AreEqual("PRIVMSG", line); + } + + [TestMethod] + public void TestTrailingSpace() + { + var line = new Line("PRIVMSG", "#channel", "hello world").Format(); + + Assert.AreEqual("PRIVMSG #channel :hello world", line); + } + + [TestMethod] + public void TestTrailingNoSpace() + { + var line = new Line("PRIVMSG", "#channel", "helloworld").Format(); + + Assert.AreEqual("PRIVMSG #channel helloworld", line); + } + + [TestMethod] + public void TestTrailingDoubleColon() + { + var line = new Line("PRIVMSG", "#channel", ":helloworld").Format(); + + Assert.AreEqual("PRIVMSG #channel ::helloworld", line); + } + + [TestMethod] + public void TestInvalidNonLastSpace() + { + Assert.ThrowsException(() => { new Line("USER", "user", "0 *", "real name").Format(); }); + } + + [TestMethod] + public void TestInvalidNonLastColon() + { + Assert.ThrowsException(() => { new Line("PRIVMSG", ":#channel", "hello").Format(); }); + } + } +} diff --git a/IRCTokens/Tests/Hostmask.cs b/IRCTokens/Tests/Hostmask.cs new file mode 100644 index 0000000..2446013 --- /dev/null +++ b/IRCTokens/Tests/Hostmask.cs @@ -0,0 +1,64 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCTokens.Tests +{ + [TestClass] + public class Hostmask + { + [TestMethod] + public void TestHostmask() + { + var hostmask = new IRCTokens.Hostmask("nick!user@host"); + Assert.AreEqual("nick", hostmask.NickName); + Assert.AreEqual("user", hostmask.UserName); + Assert.AreEqual("host", hostmask.HostName); + } + + [TestMethod] + public void TestNoHostName() + { + var hostmask = new IRCTokens.Hostmask("nick!user"); + Assert.AreEqual("nick", hostmask.NickName); + Assert.AreEqual("user", hostmask.UserName); + Assert.IsNull(hostmask.HostName); + } + + [TestMethod] + public void TestNoUserName() + { + var hostmask = new IRCTokens.Hostmask("nick@host"); + Assert.AreEqual("nick", hostmask.NickName); + Assert.IsNull(hostmask.UserName); + Assert.AreEqual("host", hostmask.HostName); + } + + [TestMethod] + public void TestOnlyNickName() + { + var hostmask = new IRCTokens.Hostmask("nick"); + Assert.AreEqual("nick", hostmask.NickName); + Assert.IsNull(hostmask.UserName); + Assert.IsNull(hostmask.HostName); + } + + [TestMethod] + public void TestHostmaskFromLine() + { + var line = new Line(":nick!user@host PRIVMSG #channel hello"); + var hostmask = new IRCTokens.Hostmask("nick!user@host"); + Assert.AreEqual(hostmask.ToString(), line.Hostmask.ToString()); + Assert.AreEqual("nick", line.Hostmask.NickName); + Assert.AreEqual("user", line.Hostmask.UserName); + Assert.AreEqual("host", line.Hostmask.HostName); + } + + [TestMethod] + public void TestEmptyHostmaskFromLine() + { + var line = new Line("PRIVMSG #channel hello"); + Assert.IsNull(line.Hostmask.HostName); + Assert.IsNull(line.Hostmask.UserName); + Assert.IsNull(line.Hostmask.NickName); + } + } +} diff --git a/IRCTokens/Tests/Parser.cs b/IRCTokens/Tests/Parser.cs new file mode 100644 index 0000000..bd0a92d --- /dev/null +++ b/IRCTokens/Tests/Parser.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using IRCTokens.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace IRCTokens.Tests +{ + [TestClass] + public class Parser + { + private static T LoadYaml(string path) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + return deserializer.Deserialize(File.ReadAllText(path)); + } + + [TestMethod] + public void TestSplit() + { + foreach (var test in LoadYaml("Tests/Data/msg-split.yaml").Tests) + { + var tokens = new Line(test.Input); + var atoms = test.Atoms; + + Assert.AreEqual(atoms.Verb.ToUpper(CultureInfo.InvariantCulture), tokens.Command, + $"command failed on: '{test.Input}'"); + Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'"); + CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'"); + CollectionAssert.AreEqual(atoms.Params ?? new List(), tokens.Params, + $"params failed on: '{test.Input}'"); + } + } + + [TestMethod] + public void TestJoin() + { + foreach (var test in LoadYaml("Tests/Data/msg-join.yaml").Tests) + { + var atoms = test.Atoms; + var line = new Line + { + Command = atoms.Verb, Params = atoms.Params, Source = atoms.Source, Tags = atoms.Tags + }.Format(); + + Assert.IsTrue(test.Matches.Contains(line), test.Description); + } + } + } +} diff --git a/IRCTokens/Tests/StatefulDecoder.cs b/IRCTokens/Tests/StatefulDecoder.cs new file mode 100644 index 0000000..d37310f --- /dev/null +++ b/IRCTokens/Tests/StatefulDecoder.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCTokens.Tests +{ + [TestClass] + public class StatefulDecoder + { + private IRCTokens.StatefulDecoder _decoder; + + [TestInitialize] + public void TestInitialize() + { + _decoder = new IRCTokens.StatefulDecoder(); + } + + [TestMethod] + public void TestPartial() + { + var lines = _decoder.Push("PRIVMSG "); + Assert.AreEqual(0, lines.Count); + + lines = _decoder.Push("#channel hello\r\n"); + Assert.AreEqual(1, lines.Count); + + var line = new Line("PRIVMSG #channel hello"); + CollectionAssert.AreEqual(new List {line}, lines); + } + + [TestMethod] + public void TestMultiple() + { + var lines = _decoder.Push("PRIVMSG #channel1 hello\r\nPRIVMSG #channel2 hello\r\n"); + Assert.AreEqual(2, lines.Count); + + var line1 = new Line("PRIVMSG #channel1 hello"); + var line2 = new Line("PRIVMSG #channel2 hello"); + Assert.AreEqual(line1, lines[0]); + Assert.AreEqual(line2, lines[1]); + } + + [TestMethod] + public void TestEncoding() + { + var iso8859 = Encoding.GetEncoding("iso-8859-1"); + _decoder = new IRCTokens.StatefulDecoder {Encoding = iso8859}; + var bytes = iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"); + var lines = _decoder.Push(bytes, bytes.Length); + var line = new Line("PRIVMSG #channel :hello Ç"); + Assert.IsTrue(line.Equals(lines[0])); + } + + [TestMethod] + public void TestEncodingFallback() + { + var latin1 = Encoding.GetEncoding("iso-8859-1"); + _decoder = new IRCTokens.StatefulDecoder {Encoding = null, Fallback = latin1}; + var bytes = latin1.GetBytes("PRIVMSG #channel hélló\r\n"); + var lines = _decoder.Push(bytes, bytes.Length); + Assert.AreEqual(1, lines.Count); + Assert.IsTrue(new Line("PRIVMSG #channel hélló").Equals(lines[0])); + } + + [TestMethod] + public void TestEmpty() + { + var lines = _decoder.Push(string.Empty); + Assert.AreEqual(0, lines.Count); + } + + [TestMethod] + public void TestBufferUnfinished() + { + _decoder.Push("PRIVMSG #channel hello"); + var lines = _decoder.Push(string.Empty); + Assert.AreEqual(0, lines.Count); + } + + [TestMethod] + public void TestClear() + { + _decoder.Push("PRIVMSG "); + _decoder.Clear(); + Assert.AreEqual(string.Empty, _decoder.Pending); + } + } +} diff --git a/IRCTokens/Tests/StatefulEncoder.cs b/IRCTokens/Tests/StatefulEncoder.cs new file mode 100644 index 0000000..5ced4d2 --- /dev/null +++ b/IRCTokens/Tests/StatefulEncoder.cs @@ -0,0 +1,84 @@ +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCTokens.Tests +{ + [TestClass] + public class StatefulEncoder + { + private IRCTokens.StatefulEncoder _encoder; + + [TestInitialize] + public void TestInitialize() + { + _encoder = new IRCTokens.StatefulEncoder(); + } + + [TestMethod] + public void TestPush() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending()); + } + + [TestMethod] + public void TestPopPartial() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + _encoder.Pop("PRIVMSG #channel hello".Length); + Assert.AreEqual("\r\n", _encoder.Pending()); + } + + [TestMethod] + public void TestPopReturned() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + _encoder.Push(line); + var lines = _encoder.Pop("PRIVMSG #channel hello\r\n".Length); + Assert.AreEqual(1, lines.Count); + Assert.AreEqual(line, lines[0]); + } + + [TestMethod] + public void TestPopNoneReturned() + { + var line = new Line("PRIVMSG #channel hello"); + _encoder.Push(line); + var lines = _encoder.Pop(1); + Assert.AreEqual(0, lines.Count); + } + + [TestMethod] + public void TestPopMultipleLines() + { + var line1 = new Line("PRIVMSG #channel1 hello"); + _encoder.Push(line1); + var line2 = new Line("PRIVMSG #channel2 hello"); + _encoder.Push(line2); + + var lines = _encoder.Pop(_encoder.Pending().Length); + Assert.AreEqual(2, lines.Count); + Assert.AreEqual(string.Empty, _encoder.Pending()); + } + + [TestMethod] + public void TestClear() + { + _encoder.Push(new Line("PRIVMSG #channel hello")); + _encoder.Clear(); + Assert.AreEqual(string.Empty, _encoder.Pending()); + } + + [TestMethod] + public void TestEncoding() + { + var iso8859 = Encoding.GetEncoding("iso-8859-1"); + _encoder = new IRCTokens.StatefulEncoder {Encoding = iso8859}; + _encoder.Push(new Line("PRIVMSG #channel :hello Ç")); + CollectionAssert.AreEqual(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"), _encoder.PendingBytes); + } + } +} diff --git a/IRCTokens/Tests/Tokenization.cs b/IRCTokens/Tests/Tokenization.cs new file mode 100644 index 0000000..03959de --- /dev/null +++ b/IRCTokens/Tests/Tokenization.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IRCTokens.Tests +{ + [TestClass] + public class Tokenization + { + [TestMethod] + public void TestTagsMissing() + { + var line = new Line("PRIVMSG #channel"); + Assert.IsNull(line.Tags); + } + + [TestMethod] + public void TestTagsMissingValue() + { + var line = new Line("@id= PRIVMSG #channel"); + Assert.AreEqual(string.Empty, line.Tags["id"]); + } + + [TestMethod] + public void TestTagsMissingEqual() + { + var line = new Line("@id PRIVMSG #channel"); + Assert.AreEqual(string.Empty, line.Tags["id"]); + } + + [TestMethod] + public void TestTagsUnescape() + { + var line = new Line(@"@id=1\\\:\r\n\s2 PRIVMSG #channel"); + Assert.AreEqual("1\\;\r\n 2", line.Tags["id"]); + } + + [TestMethod] + public void TestTagsOverlap() + { + var line = new Line(@"@id=1\\\s\\s PRIVMSG #channel"); + Assert.AreEqual("1\\ \\s", line.Tags["id"]); + } + + [TestMethod] + public void TestTagsLoneEndSlash() + { + var line = new Line("@id=1\\ PRIVMSG #channel"); + Assert.AreEqual("1", line.Tags["id"]); + } + + [TestMethod] + public void TestSourceWithoutTags() + { + var line = new Line(":nick!user@host PRIVMSG #channel"); + Assert.AreEqual("nick!user@host", line.Source); + } + + [TestMethod] + public void TestSourceWithTags() + { + var line = new Line("@id=123 :nick!user@host PRIVMSG #channel"); + Assert.AreEqual("nick!user@host", line.Source); + } + + [TestMethod] + public void TestSourceMissingWithoutTags() + { + var line = new Line("PRIVMSG #channel"); + Assert.IsNull(line.Source); + } + + [TestMethod] + public void TestSourceMissingWithTags() + { + var line = new Line("@id=123 PRIVMSG #channel"); + Assert.IsNull(line.Source); + } + + [TestMethod] + public void TestCommand() + { + var line = new Line("privmsg #channel"); + Assert.AreEqual("PRIVMSG", line.Command); + } + + [TestMethod] + public void TestParamsTrailing() + { + var line = new Line("PRIVMSG #channel :hello world"); + CollectionAssert.AreEqual(new List {"#channel", "hello world"}, line.Params); + } + + [TestMethod] + public void TestParamsOnlyTrailing() + { + var line = new Line("PRIVMSG :hello world"); + CollectionAssert.AreEqual(new List {"hello world"}, line.Params); + } + + [TestMethod] + public void TestParamsMissing() + { + var line = new Line("PRIVMSG"); + Assert.AreEqual("PRIVMSG", line.Command); + CollectionAssert.AreEqual(new List(), line.Params); + } + + [TestMethod] + public void TestAllTokens() + { + var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world"); + CollectionAssert.AreEqual(new Dictionary {{"id", "123"}}, line.Tags); + Assert.AreEqual("nick!user@host", line.Source); + Assert.AreEqual("PRIVMSG", line.Command); + CollectionAssert.AreEqual(new List {"#channel", "hello world"}, line.Params); + } + } +} diff --git a/IrcSharp.sln b/IrcSharp.sln deleted file mode 100644 index 8174c7c..0000000 --- a/IrcSharp.sln +++ /dev/null @@ -1,48 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30011.22 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IrcTokens", "IrcTokens\IrcTokens.csproj", "{9E812F45-B2CD-42D2-8378-EBEBF8697905}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokensSample", "Examples\Tokens\TokensSample.csproj", "{A45DA39B-6B47-4713-8049-3B36E0235B67}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IrcStates", "IrcStates\IrcStates.csproj", "{233E3CB4-61F1-4368-9139-7E9F4A58ED2D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatesSample", "Examples\States\StatesSample.csproj", "{BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A85EB22-D7B4-417F-AC3B-DAFD97DDEA08}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.Build.0 = Release|Any CPU - {A45DA39B-6B47-4713-8049-3B36E0235B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A45DA39B-6B47-4713-8049-3B36E0235B67}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A45DA39B-6B47-4713-8049-3B36E0235B67}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A45DA39B-6B47-4713-8049-3B36E0235B67}.Release|Any CPU.Build.0 = Release|Any CPU - {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {233E3CB4-61F1-4368-9139-7E9F4A58ED2D}.Release|Any CPU.Build.0 = Release|Any CPU - {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BC9F6696-9D83-4F7A-9E15-CE4D3626C1AF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0B91F0EA-8564-4318-8EEC-ED0640475141} - EndGlobalSection -EndGlobal diff --git a/IrcStates/Casemap.cs b/IrcStates/Casemap.cs deleted file mode 100644 index 67867c5..0000000 --- a/IrcStates/Casemap.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 21ebb25..0000000 --- a/IrcStates/Channel.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index fab881c..0000000 --- a/IrcStates/ChannelUser.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index b5bc358..0000000 --- a/IrcStates/Commands.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 9ca9cb5..0000000 --- a/IrcStates/Emit.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 181ac80..0000000 --- a/IrcStates/Extensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace IrcStates -{ - public static class Extensions - { - public static Delegate CreateDelegate(this MethodInfo methodInfo, object target) - { - if (methodInfo == null) return null; - - var types = methodInfo.GetParameters().Select(p => p.ParameterType); - - Func getType; - if (methodInfo.ReturnType == typeof(void)) - { - getType = Expression.GetActionType; - } - else - { - getType = Expression.GetFuncType; - types = types.Concat(new[] {methodInfo.ReturnType}); - } - - return methodInfo.IsStatic - ? Delegate.CreateDelegate(getType(types.ToArray()), methodInfo) - : Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo); - } - - 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/ISupport.cs b/IrcStates/ISupport.cs deleted file mode 100644 index f481f5f..0000000 --- a/IrcStates/ISupport.cs +++ /dev/null @@ -1,114 +0,0 @@ -// 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 deleted file mode 100644 index 74a0579..0000000 --- a/IrcStates/ISupportChanModes.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index fb5114b..0000000 --- a/IrcStates/ISupportPrefix.cs +++ /dev/null @@ -1,44 +0,0 @@ -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/IrcStates.csproj b/IrcStates/IrcStates.csproj deleted file mode 100644 index 7500c8b..0000000 --- a/IrcStates/IrcStates.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netcoreapp3.1 - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - diff --git a/IrcStates/Numeric.cs b/IrcStates/Numeric.cs deleted file mode 100644 index 8639c12..0000000 --- a/IrcStates/Numeric.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 05daa8c..0000000 --- a/IrcStates/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# 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 deleted file mode 100644 index 2e80b75..0000000 --- a/IrcStates/Server.cs +++ /dev/null @@ -1,941 +0,0 @@ -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 deleted file mode 100644 index c3e014f..0000000 --- a/IrcStates/ServerDisconnectedException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace IrcStates -{ - public class ServerDisconnectedException : Exception - { - } -} diff --git a/IrcStates/ServerException.cs b/IrcStates/ServerException.cs deleted file mode 100644 index 7164e76..0000000 --- a/IrcStates/ServerException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace IrcStates -{ - public class ServerException : Exception - { - } -} diff --git a/IrcStates/Tests/Cap.cs b/IrcStates/Tests/Cap.cs deleted file mode 100644 index 3ce52f8..0000000 --- a/IrcStates/Tests/Cap.cs +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index 6022593..0000000 --- a/IrcStates/Tests/Casemap.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 64a845c..0000000 --- a/IrcStates/Tests/Channel.cs +++ /dev/null @@ -1,202 +0,0 @@ -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 deleted file mode 100644 index 15e9e61..0000000 --- a/IrcStates/Tests/Emit.cs +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 6cd2f48..0000000 --- a/IrcStates/Tests/ISupport.cs +++ /dev/null @@ -1,210 +0,0 @@ -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 deleted file mode 100644 index 799afd6..0000000 --- a/IrcStates/Tests/Mode.cs +++ /dev/null @@ -1,179 +0,0 @@ -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 deleted file mode 100644 index 92929a5..0000000 --- a/IrcStates/Tests/Motd.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index f61e2e1..0000000 --- a/IrcStates/Tests/Sasl.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 5857cfc..0000000 --- a/IrcStates/Tests/User.cs +++ /dev/null @@ -1,298 +0,0 @@ -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 deleted file mode 100644 index 7e4e323..0000000 --- a/IrcStates/Tests/Who.cs +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 7df331b..0000000 --- a/IrcStates/User.cs +++ /dev/null @@ -1,33 +0,0 @@ -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; - } - } -} diff --git a/IrcTokens/Extensions.cs b/IrcTokens/Extensions.cs deleted file mode 100644 index 4b23774..0000000 --- a/IrcTokens/Extensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace IrcTokens -{ - public static class Extensions - { - public static IEnumerable Split(this byte[] bytes, byte separator) - { - if (bytes == null || bytes.Length == 0) return new List(); - - var newLineIndices = bytes.Select((b, i) => b == separator ? i : -1).Where(i => i != -1).ToArray(); - var lines = new byte[newLineIndices.Length + 1][]; - var currentIndex = 0; - var arrIndex = 0; - - for (var i = 0; i < newLineIndices.Length && currentIndex < bytes.Length; ++i) - { - var n = new byte[newLineIndices[i] - currentIndex]; - Array.Copy(bytes, currentIndex, n, 0, newLineIndices[i] - currentIndex); - currentIndex = newLineIndices[i] + 1; - lines[arrIndex++] = n; - } - - // Handle the last string at the end of the array if there is one. - if (currentIndex < bytes.Length) - lines[arrIndex] = bytes.Skip(currentIndex).ToArray(); - else if (arrIndex == newLineIndices.Length) - // We had a separator character at the end of a string. Rather than just allowing - // a null character, we'll replace the last element in the array with an empty string. - lines[arrIndex] = Array.Empty(); - - return lines.ToArray(); - } - - public static byte[] Trim(this IEnumerable bytes, byte separator) - { - if (bytes == null) return Array.Empty(); - - var byteList = new List(bytes); - var i = 0; - - if (!byteList.Any()) return byteList.ToArray(); - - while (byteList[i] == separator) - { - byteList.RemoveAt(i); - i++; - } - - i = byteList.Count - 1; - while (byteList[i] == separator) - { - byteList.RemoveAt(i); - i--; - } - - return byteList.ToArray(); - } - } -} diff --git a/IrcTokens/Hostmask.cs b/IrcTokens/Hostmask.cs deleted file mode 100644 index 3c0b7f2..0000000 --- a/IrcTokens/Hostmask.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; - -namespace IrcTokens -{ - /// - /// Represents the three parts of a hostmask. Parse with the constructor. - /// - public class Hostmask : IEquatable - { - private readonly string _source; - - public Hostmask(string source) - { - if (source == null) return; - - _source = source; - - if (source.Contains('@', StringComparison.Ordinal)) - { - var split = source.Split('@'); - - NickName = split[0]; - HostName = split[1]; - } - else - { - NickName = source; - } - - if (NickName.Contains('!', StringComparison.Ordinal)) - { - var userSplit = NickName.Split('!'); - NickName = userSplit[0]; - UserName = userSplit[1]; - } - } - - public string NickName { get; set; } - public string UserName { get; set; } - public string HostName { get; set; } - - public bool Equals(Hostmask other) - { - if (other == null) return false; - - return _source == other._source; - } - - public override string ToString() - { - return _source; - } - - public override int GetHashCode() - { - return _source.GetHashCode(StringComparison.Ordinal); - } - - public override bool Equals(object obj) - { - return Equals(obj as Hostmask); - } - } -} diff --git a/IrcTokens/IrcTokens.csproj b/IrcTokens/IrcTokens.csproj deleted file mode 100644 index 2fe9300..0000000 --- a/IrcTokens/IrcTokens.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - netcoreapp3.1 - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - diff --git a/IrcTokens/Line.cs b/IrcTokens/Line.cs deleted file mode 100644 index 36899bf..0000000 --- a/IrcTokens/Line.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; - -namespace IrcTokens -{ - /// - /// Tools to represent, parse, and format IRC lines - /// - public class Line : IEquatable - { - private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"}; - - private static readonly string[] TagEscaped = {"\\\\", "\\s", "\\:", "\\r", "\\n"}; - - private Hostmask _hostmask; - - public Line() - { - } - - public Line(string command, params string[] parameters) - { - Command = command; - Params = parameters.ToList(); - } - - /// - /// Build new object parsed from - /// a string - /// . Analogous to irctokens.tokenise() - /// - /// irc line to parse - public Line(string line) - { - if (string.IsNullOrWhiteSpace(line)) throw new ArgumentNullException(nameof(line)); - - string[] split; - - if (line.StartsWith('@')) - { - Tags = new Dictionary(); - - split = line.Split(" ", 2); - var messageTags = split[0]; - line = split[1]; - - foreach (var part in messageTags.Substring(1).Split(';')) - if (part.Contains('=', StringComparison.Ordinal)) - { - split = part.Split('=', 2); - Tags[split[0]] = UnescapeTag(split[1]); - } - else - { - Tags[part] = string.Empty; - } - } - - string trailing; - if (line.Contains(" :", StringComparison.Ordinal)) - { - split = line.Split(" :", 2); - line = split[0]; - trailing = split[1]; - } - else - { - trailing = null; - } - - Params = line.Contains(' ', StringComparison.Ordinal) - ? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList() - : new List {line}; - - if (Params[0].StartsWith(':')) - { - Source = Params[0].Substring(1); - Params.RemoveAt(0); - } - - if (Params.Count > 0) - { - Command = Params[0].ToUpper(CultureInfo.InvariantCulture); - Params.RemoveAt(0); - } - - if (trailing != null) Params.Add(trailing); - } - - public Dictionary Tags { get; set; } - public string Source { get; set; } - public string Command { get; set; } - public List Params { get; set; } - - public Hostmask Hostmask => - _hostmask ??= new Hostmask(Source); - - public bool Equals(Line other) - { - if (other == null) return false; - - return Format() == other.Format(); - } - - /// - /// Unescape ircv3 tag - /// - /// escaped string - /// unescaped string - private static string UnescapeTag(string val) - { - var unescaped = new StringBuilder(); - - var graphemeIterator = StringInfo.GetTextElementEnumerator(val); - graphemeIterator.Reset(); - - while (graphemeIterator.MoveNext()) - { - var current = graphemeIterator.GetTextElement(); - - if (current == @"\") - try - { - graphemeIterator.MoveNext(); - var next = graphemeIterator.GetTextElement(); - var pair = current + next; - unescaped.Append(TagEscaped.Contains(pair) - ? TagUnescaped[Array.IndexOf(TagEscaped, pair)] - : next); - } - catch (InvalidOperationException) - { - // ignored - } - else - unescaped.Append(current); - } - - return unescaped.ToString(); - } - - /// - /// Escape strings for use in ircv3 tags - /// - /// string to escape - /// escaped string - private static string EscapeTag(string val) - { - for (var i = 0; i < TagUnescaped.Length; ++i) - val = val?.Replace(TagUnescaped[i], TagEscaped[i], StringComparison.Ordinal); - - return val; - } - - public override string ToString() - { - var vars = new List(); - - if (Command != null) vars.Add($"command={Command}"); - - if (Source != null) vars.Add($"source={Source}"); - - if (Params != null && Params.Any()) vars.Add($"params=[{string.Join(",", Params)}]"); - - if (Tags != null && Tags.Any()) - vars.Add($"tags=[{string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]"); - - return $"Line({string.Join(", ", vars)})"; - } - - public override int GetHashCode() - { - return Format().GetHashCode(StringComparison.Ordinal); - } - - public override bool Equals(object obj) - { - return Equals(obj as Line); - } - - /// - /// Format a as a standards-compliant IRC line - /// - /// formatted irc line - public string Format() - { - var outs = new List(); - - if (Tags != null && Tags.Any()) - { - var tags = Tags.Keys - .OrderBy(k => k) - .Select(key => - string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={EscapeTag(Tags[key])}") - .ToList(); - - outs.Add($"@{string.Join(";", tags)}"); - } - - if (Source != null) outs.Add($":{Source}"); - - outs.Add(Command); - - if (Params != null && Params.Any()) - { - var last = Params[^1]; - var withoutLast = Params.SkipLast(1).ToList(); - - foreach (var p in withoutLast) - { - if (p.Contains(' ', StringComparison.Ordinal)) - throw new ArgumentException(@"non-last parameters cannot have spaces", p); - - if (p.StartsWith(':')) - throw new ArgumentException(@"non-last parameters cannot start with colon", p); - } - - outs.AddRange(withoutLast); - - if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) || - last.StartsWith(':')) - last = $":{last}"; - - outs.Add(last); - } - - return string.Join(" ", outs); - } - } -} diff --git a/IrcTokens/README.md b/IrcTokens/README.md deleted file mode 100644 index 3981654..0000000 --- a/IrcTokens/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# irctokens - -this is a c\# port of jesopo's [irctokens]( -https://github.com/jesopo/irctokens) - -## usage - -### tokenization - - using IrcTokens; - - ... - - var line = new Line("@id=123 :ben!~ben@host.tld PRIVMSG #channel :hello there!"); - Console.WriteLine(line); - Console.WriteLine(line.Format()); - -### formatting - - var line = new Line {Command = "USER", Params = new List {"user", "0", "*", "real name"}}; - Console.WriteLine(line); - Console.WriteLine(line.Format()); - -### stateful - -see the full example in [TokensSample/Client.cs](../Examples/Tokens/Client.cs) - - public class Client - { - private readonly byte[] _bytes; - private readonly StatefulDecoder _decoder; - private readonly StatefulEncoder _encoder; - private readonly Socket _socket; - - public Client() - { - _decoder = new StatefulDecoder(); - _encoder = new StatefulEncoder(); - _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); - _bytes = new byte[1024]; - } - - public void Start() - { - _socket.Connect("127.0.0.1", 6667); - while (!_socket.Connected) Thread.Sleep(1000); - - Send(new Line {Command = "NICK", Params = new List {"tokensbot"}}); - Send(new Line {Command = "USER", Params = new List {"tokensbot", "0", "*", "real name"}}); - - while (true) - { - var bytesReceived = _socket.Receive(_bytes); - - if (bytesReceived == 0) - { - Console.WriteLine("! disconnected"); - _socket.Shutdown(SocketShutdown.Both); - _socket.Close(); - break; - } - - var lines = _decoder.Push(_bytes, bytesReceived); - - foreach (var line in lines) - { - Console.WriteLine($"< {line.Format()}"); - - switch (line.Command) - { - case "PING": - Send(new Line {Command = "PONG", Params = line.Params}); - break; - case "001": - Send(new Line {Command = "JOIN", Params = new List {"#test"}}); - break; - case "PRIVMSG": - Send(new Line - { - Command = "PRIVMSG", Params = new List {line.Params[0], "hello there"} - }); - break; - } - } - } - } - - private void Send(Line line) - { - Console.WriteLine($"> {line.Format()}"); - _encoder.Push(line); - while (_encoder.PendingBytes.Length > 0) - _encoder.Pop(_socket.Send(_encoder.PendingBytes, SocketFlags.None)); - } - } - diff --git a/IrcTokens/StatefulDecoder.cs b/IrcTokens/StatefulDecoder.cs deleted file mode 100644 index 1f6636b..0000000 --- a/IrcTokens/StatefulDecoder.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace IrcTokens -{ - public class StatefulDecoder - { - private byte[] _buffer; - private Encoding _encoding; - private Encoding _fallback; - - public StatefulDecoder() - { - Clear(); - } - - public Encoding Encoding - { - get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback, - DecoderFallback.ExceptionFallback); - set - { - if (value != null) - _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback, - DecoderFallback.ReplacementFallback); - } - } - - public Encoding Fallback - { - get => _fallback ?? Encoding.GetEncoding(Encoding.GetEncoding("iso-8859-1").CodePage, - EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); - set - { - if (value != null) - _fallback = Encoding.GetEncoding(value.CodePage, EncoderFallback.ReplacementFallback, - DecoderFallback.ReplacementFallback); - } - } - - public string Pending => Encoding.GetString(_buffer); - - public void Clear() - { - _buffer = Array.Empty(); - } - - public List Push(string data) - { - var bytes = Encoding.GetBytes(data); - return Push(bytes, bytes.Length); - } - - public List Push(byte[] data, int bytesReceived) - { - if (data == null) return null; - - _buffer = _buffer == null ? Array.Empty() : _buffer.Concat(data.Take(bytesReceived)).ToArray(); - - var listLines = _buffer.Split((byte) '\n').Select(l => l.Trim((byte) '\r')).ToList(); - _buffer = listLines.LastOrDefault() ?? Array.Empty(); - - var decodeLines = new List(); - foreach (var line in listLines.SkipLast(1).Select(l => l.ToArray())) - try - { - decodeLines.Add(new Line(Encoding.GetString(line))); - } - catch (DecoderFallbackException) - { - decodeLines.Add(new Line(Fallback.GetString(line))); - } - - return decodeLines; - } - } -} diff --git a/IrcTokens/StatefulEncoder.cs b/IrcTokens/StatefulEncoder.cs deleted file mode 100644 index bec4e42..0000000 --- a/IrcTokens/StatefulEncoder.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace IrcTokens -{ - public class StatefulEncoder - { - private List _bufferedLines; - private Encoding _encoding; - - public StatefulEncoder() - { - Clear(); - } - - public Encoding Encoding - { - get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback, - DecoderFallback.ExceptionFallback); - set - { - if (value != null) - _encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback, - DecoderFallback.ExceptionFallback); - } - } - - public byte[] PendingBytes { get; private set; } - - public string Pending() - { - try - { - return Encoding.GetString(PendingBytes); - } - catch (DecoderFallbackException e) - { - Console.WriteLine(e); - throw; - } - } - - public void Clear() - { - PendingBytes = Array.Empty(); - _bufferedLines = new List(); - } - - public void Push(Line line) - { - if (line == null) throw new ArgumentNullException(nameof(line)); - - PendingBytes = PendingBytes.Concat(Encoding.GetBytes($"{line.Format()}\r\n")).ToArray(); - _bufferedLines.Add(line); - } - - public List Pop(int byteCount) - { - var sent = PendingBytes.Take(byteCount).Count(c => c == '\n'); - - PendingBytes = PendingBytes.Skip(byteCount).ToArray(); - - var sentLines = _bufferedLines.Take(sent).ToList(); - _bufferedLines = _bufferedLines.Skip(sent).ToList(); - - return sentLines; - } - } -} diff --git a/IrcTokens/Tests/Data/JoinModel.cs b/IrcTokens/Tests/Data/JoinModel.cs deleted file mode 100644 index 2c08e58..0000000 --- a/IrcTokens/Tests/Data/JoinModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using YamlDotNet.Serialization; - -namespace IrcTokens.Tests.Data -{ - public class JoinModel - { - public List Tests { get; set; } - - public class Test - { - [YamlMember(Alias = "desc")] public string Description { get; set; } - - public Atoms Atoms { get; set; } - - public List Matches { get; set; } - } - - public class Atoms - { - public Dictionary Tags { get; set; } - - public string Source { get; set; } - - public string Verb { get; set; } - - public List Params { get; set; } - } - } -} diff --git a/IrcTokens/Tests/Data/SplitModel.cs b/IrcTokens/Tests/Data/SplitModel.cs deleted file mode 100644 index 65177a3..0000000 --- a/IrcTokens/Tests/Data/SplitModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace IrcTokens.Tests.Data -{ - public class SplitModel - { - public List Tests { get; set; } - - public class Test - { - public string Input { get; set; } - public JoinModel.Atoms Atoms { get; set; } - } - } -} diff --git a/IrcTokens/Tests/Data/msg-join.yaml b/IrcTokens/Tests/Data/msg-join.yaml deleted file mode 100644 index d1d7429..0000000 --- a/IrcTokens/Tests/Data/msg-join.yaml +++ /dev/null @@ -1,221 +0,0 @@ -# IRC parser tests -# joining atoms into sendable messages - -# Written in 2015 by Daniel Oaks -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along -# with this software. If not, see -# . - -# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed -# https://github.com/grawity/code/tree/master/lib/tests -# some of the tests here originate from Mozilla's test vectors, which is public domain -# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js -# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here -# https://github.com/SaberUK/ircparser/tree/master/test - -tests: - # the desc string holds a description of the test, if it exists - - # the atoms dict has the keys: - # * tags: tags dict - # tags with no value are an empty string - # * source: source string, without single leading colon - # * verb: verb string - # * params: params split up as a list - # if the params key does not exist, assume it is empty - # if any other keys do no exist, assume they are null - # a key that is null does not exist or is not specified with the - # given input string - - # matches is a list of messages that match - - # simple tests - - desc: Simple test with verb and params. - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "asdf" - matches: - - "foo bar baz asdf" - - "foo bar baz :asdf" - - # with no regular params - - desc: Simple test with source and no params. - atoms: - source: "src" - verb: "AWAY" - matches: - - ":src AWAY" - - - desc: Simple test with source and empty trailing param. - atoms: - source: "src" - verb: "AWAY" - params: - - "" - matches: - - ":src AWAY :" - - # with source - - desc: Simple test with source. - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "asdf" - matches: - - ":coolguy foo bar baz asdf" - - ":coolguy foo bar baz :asdf" - - # with trailing param - - desc: Simple test with trailing param. - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "asdf quux" - matches: - - "foo bar baz :asdf quux" - - - desc: Simple test with empty trailing param. - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "" - matches: - - "foo bar baz :" - - - desc: Simple test with trailing param containing colon. - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - ":asdf" - matches: - - "foo bar baz ::asdf" - - # with source and trailing param - - desc: Test with source and trailing param. - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "asdf quux" - matches: - - ":coolguy foo bar baz :asdf quux" - - - desc: Test with trailing containing beginning+end whitespace. - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - " asdf quux " - matches: - - ":coolguy foo bar baz : asdf quux " - - - desc: Test with trailing containing what looks like another trailing param. - atoms: - source: "coolguy" - verb: "PRIVMSG" - params: - - "bar" - - "lol :) " - matches: - - ":coolguy PRIVMSG bar :lol :) " - - - desc: Simple test with source and empty trailing. - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "" - matches: - - ":coolguy foo bar baz :" - - - desc: Trailing contains only spaces. - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - " " - matches: - - ":coolguy foo bar baz : " - - - desc: Param containing tab (tab is not considered SPACE for message splitting). - atoms: - source: "coolguy" - verb: "foo" - params: - - "b\tar" - - "baz" - matches: - - ":coolguy foo b\tar baz" - - ":coolguy foo b\tar :baz" - - # with tags - - desc: Tag with no value and space-filled trailing. - atoms: - tags: - "asd": "" - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - " " - matches: - - "@asd :coolguy foo bar baz : " - - - desc: Tags with escaped values. - atoms: - verb: "foo" - tags: - "a": "b\\and\nk" - "d": "gh;764" - matches: - - "@a=b\\\\and\\nk;d=gh\\:764 foo" - - "@d=gh\\:764;a=b\\\\and\\nk foo" - - - desc: Tags with escaped values and params. - atoms: - verb: "foo" - tags: - "a": "b\\and\nk" - "d": "gh;764" - params: - - "par1" - - "par2" - matches: - - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" - - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" - - "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" - - "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2" - - - desc: Tag with long, strange values (including LF and newline). - atoms: - tags: - foo: "\\\\;\\s \r\n" - verb: "COMMAND" - matches: - - "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" diff --git a/IrcTokens/Tests/Data/msg-split.yaml b/IrcTokens/Tests/Data/msg-split.yaml deleted file mode 100644 index fa3f4aa..0000000 --- a/IrcTokens/Tests/Data/msg-split.yaml +++ /dev/null @@ -1,343 +0,0 @@ -# IRC parser tests -# splitting messages into usable atoms - -# Written in 2015 by Daniel Oaks -# -# To the extent possible under law, the author(s) have dedicated all copyright -# and related and neighboring rights to this software to the public domain -# worldwide. This software is distributed without any warranty. -# -# You should have received a copy of the CC0 Public Domain Dedication along -# with this software. If not, see -# . - -# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed -# https://github.com/grawity/code/tree/master/lib/tests -# some of the tests here originate from Mozilla's test vectors, which is public domain -# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js -# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here -# https://github.com/SaberUK/ircparser/tree/master/test - -# we follow RFC1459 with regards to multiple ascii spaces splitting atoms: -# The prefix, command, and all parameters are -# separated by one (or more) ASCII space character(s) (0x20). -# because doing it as RFC2812 says (strictly as a single ascii space) isn't sane - -tests: - # input is the string coming directly from the server to parse - - # the atoms dict has the keys: - # * tags: tags dict - # tags with no value are an empty string - # * source: source string, without single leading colon - # * verb: verb string - # * params: params split up as a list - # if the params key does not exist, assume it is empty - # if any other keys do no exist, assume they are null - # a key that is null does not exist or is not specified with the - # given input string - - # simple - - input: "foo bar baz asdf" - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "asdf" - - # with source - - input: ":coolguy foo bar baz asdf" - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "asdf" - - # with trailing param - - input: "foo bar baz :asdf quux" - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "asdf quux" - - - input: "foo bar baz :" - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - "" - - - input: "foo bar baz ::asdf" - atoms: - verb: "foo" - params: - - "bar" - - "baz" - - ":asdf" - - # with source and trailing param - - input: ":coolguy foo bar baz :asdf quux" - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "asdf quux" - - - input: ":coolguy foo bar baz : asdf quux " - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - " asdf quux " - - - input: ":coolguy PRIVMSG bar :lol :) " - atoms: - source: "coolguy" - verb: "PRIVMSG" - params: - - "bar" - - "lol :) " - - - input: ":coolguy foo bar baz :" - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - "" - - - input: ":coolguy foo bar baz : " - atoms: - source: "coolguy" - verb: "foo" - params: - - "bar" - - "baz" - - " " - - # with tags - - input: "@a=b;c=32;k;rt=ql7 foo" - atoms: - verb: "foo" - tags: - "a": "b" - "c": "32" - "k": "" - "rt": "ql7" - - # with escaped tags - - input: "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo" - atoms: - verb: "foo" - tags: - "a": "b\\and\nk" - "c": "72 45" - "d": "gh;764" - - # with tags and source - - input: "@c;h=;a=b :quux ab cd" - atoms: - tags: - "c": "" - "h": "" - "a": "b" - source: "quux" - verb: "ab" - params: - - "cd" - - # different forms of last param - - input: ":src JOIN #chan" - atoms: - source: "src" - verb: "JOIN" - params: - - "#chan" - - - input: ":src JOIN :#chan" - atoms: - source: "src" - verb: "JOIN" - params: - - "#chan" - - # with and without last param - - input: ":src AWAY" - atoms: - source: "src" - verb: "AWAY" - - - input: ":src AWAY " - atoms: - source: "src" - verb: "AWAY" - - # tab is not considered - - input: ":cool\tguy foo bar baz" - atoms: - source: "cool\tguy" - verb: "foo" - params: - - "bar" - - "baz" - - # with weird control codes in the source - - input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz" - atoms: - source: "coolguy!ag@net\x035w\x03ork.admin" - verb: "PRIVMSG" - params: - - "foo" - - "bar baz" - - - input: ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz" - atoms: - source: "coolguy!~ag@n\x02et\x0305w\x0fork.admin" - verb: "PRIVMSG" - params: - - "foo" - - "bar baz" - - - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3" - atoms: - tags: - tag1: "value1" - tag2: "" - vendor1/tag3: "value2" - vendor2/tag4: "" - source: "irc.example.com" - verb: "COMMAND" - params: - - "param1" - - "param2" - - "param3 param3" - - - input: ":irc.example.com COMMAND param1 param2 :param3 param3" - atoms: - source: "irc.example.com" - verb: "COMMAND" - params: - - "param1" - - "param2" - - "param3 param3" - - - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3" - atoms: - tags: - tag1: "value1" - tag2: "" - vendor1/tag3: "value2" - vendor2/tag4: "" - verb: "COMMAND" - params: - - "param1" - - "param2" - - "param3 param3" - - - input: "COMMAND" - atoms: - verb: "COMMAND" - - # yaml encoding + slashes is fun - - input: "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" - atoms: - tags: - foo: "\\\\;\\s \r\n" - verb: "COMMAND" - - # broken messages from unreal - - input: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters" - atoms: - source: "gravel.mozilla.org" - verb: "432" - params: - - "#momo" - - "Erroneous Nickname: Illegal characters" - - - input: ":gravel.mozilla.org MODE #tckk +n " - atoms: - source: "gravel.mozilla.org" - verb: "MODE" - params: - - "#tckk" - - "+n" - - - input: ":services.esper.net MODE #foo-bar +o foobar " - atoms: - source: "services.esper.net" - verb: "MODE" - params: - - "#foo-bar" - - "+o" - - "foobar" - - # tag values should be parsed char-at-a-time to prevent wayward replacements. - - input: "@tag1=value\\\\ntest COMMAND" - atoms: - tags: - tag1: "value\\ntest" - verb: "COMMAND" - - # If a tag value has a slash followed by a character which doesn't need - # to be escaped, the slash should be dropped. - - input: "@tag1=value\\1 COMMAND" - atoms: - tags: - tag1: "value1" - verb: "COMMAND" - - # A slash at the end of a tag value should be dropped - - input: "@tag1=value1\\ COMMAND" - atoms: - tags: - tag1: "value1" - verb: "COMMAND" - - # Duplicate tags: Parsers SHOULD disregard all but the final occurence - - input: "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND" - atoms: - tags: - tag1: "5" - tag2: "3" - tag3: "4" - verb: "COMMAND" - - # vendored tags can have the same name as a non-vendored tag - - input: "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND" - atoms: - tags: - tag1: "5" - tag2: "3" - tag3: "4" - vendor/tag2: "8" - verb: "COMMAND" - - # Some parsers handle /MODE in a special way, make sure they do it right - - input: ":SomeOp MODE #channel :+i" - atoms: - source: "SomeOp" - verb: "MODE" - params: - - "#channel" - - "+i" - - - input: ":SomeOp MODE #channel +oo SomeUser :AnotherUser" - atoms: - source: "SomeOp" - verb: "MODE" - params: - - "#channel" - - "+oo" - - "SomeUser" - - "AnotherUser" diff --git a/IrcTokens/Tests/Format.cs b/IrcTokens/Tests/Format.cs deleted file mode 100644 index 69a5682..0000000 --- a/IrcTokens/Tests/Format.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace IrcTokens.Tests -{ - [TestClass] - public class Format - { - [TestMethod] - public void TestTags() - { - var line = new Line("PRIVMSG", "#channel", "hello") - { - Tags = new Dictionary {{"id", "\\" + " " + ";" + "\r\n"}} - }.Format(); - - Assert.AreEqual("@id=\\\\\\s\\:\\r\\n PRIVMSG #channel hello", line); - } - - [TestMethod] - public void TestMissingTag() - { - var line = new Line("PRIVMSG", "#channel", "hello").Format(); - - Assert.AreEqual("PRIVMSG #channel hello", line); - } - - [TestMethod] - public void TestNullTag() - { - var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary {{"a", null}}} - .Format(); - - Assert.AreEqual("@a PRIVMSG #channel hello", line); - } - - [TestMethod] - public void TestEmptyTag() - { - var line = new Line("PRIVMSG", "#channel", "hello") {Tags = new Dictionary {{"a", ""}}} - .Format(); - - Assert.AreEqual("@a PRIVMSG #channel hello", line); - } - - [TestMethod] - public void TestSource() - { - var line = new Line("PRIVMSG", "#channel", "hello") {Source = "nick!user@host"}.Format(); - - Assert.AreEqual(":nick!user@host PRIVMSG #channel hello", line); - } - - [TestMethod] - public void TestCommandLowercase() - { - var line = new Line {Command = "privmsg"}.Format(); - Assert.AreEqual("privmsg", line); - } - - [TestMethod] - public void TestCommandUppercase() - { - var line = new Line {Command = "PRIVMSG"}.Format(); - Assert.AreEqual("PRIVMSG", line); - } - - [TestMethod] - public void TestTrailingSpace() - { - var line = new Line("PRIVMSG", "#channel", "hello world").Format(); - - Assert.AreEqual("PRIVMSG #channel :hello world", line); - } - - [TestMethod] - public void TestTrailingNoSpace() - { - var line = new Line("PRIVMSG", "#channel", "helloworld").Format(); - - Assert.AreEqual("PRIVMSG #channel helloworld", line); - } - - [TestMethod] - public void TestTrailingDoubleColon() - { - var line = new Line("PRIVMSG", "#channel", ":helloworld").Format(); - - Assert.AreEqual("PRIVMSG #channel ::helloworld", line); - } - - [TestMethod] - public void TestInvalidNonLastSpace() - { - Assert.ThrowsException(() => { new Line("USER", "user", "0 *", "real name").Format(); }); - } - - [TestMethod] - public void TestInvalidNonLastColon() - { - Assert.ThrowsException(() => { new Line("PRIVMSG", ":#channel", "hello").Format(); }); - } - } -} diff --git a/IrcTokens/Tests/Hostmask.cs b/IrcTokens/Tests/Hostmask.cs deleted file mode 100644 index 6a5cf65..0000000 --- a/IrcTokens/Tests/Hostmask.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace IrcTokens.Tests -{ - [TestClass] - public class Hostmask - { - [TestMethod] - public void TestHostmask() - { - var hostmask = new IrcTokens.Hostmask("nick!user@host"); - Assert.AreEqual("nick", hostmask.NickName); - Assert.AreEqual("user", hostmask.UserName); - Assert.AreEqual("host", hostmask.HostName); - } - - [TestMethod] - public void TestNoHostName() - { - var hostmask = new IrcTokens.Hostmask("nick!user"); - Assert.AreEqual("nick", hostmask.NickName); - Assert.AreEqual("user", hostmask.UserName); - Assert.IsNull(hostmask.HostName); - } - - [TestMethod] - public void TestNoUserName() - { - var hostmask = new IrcTokens.Hostmask("nick@host"); - Assert.AreEqual("nick", hostmask.NickName); - Assert.IsNull(hostmask.UserName); - Assert.AreEqual("host", hostmask.HostName); - } - - [TestMethod] - public void TestOnlyNickName() - { - var hostmask = new IrcTokens.Hostmask("nick"); - Assert.AreEqual("nick", hostmask.NickName); - Assert.IsNull(hostmask.UserName); - Assert.IsNull(hostmask.HostName); - } - - [TestMethod] - public void TestHostmaskFromLine() - { - var line = new Line(":nick!user@host PRIVMSG #channel hello"); - var hostmask = new IrcTokens.Hostmask("nick!user@host"); - Assert.AreEqual(hostmask.ToString(), line.Hostmask.ToString()); - Assert.AreEqual("nick", line.Hostmask.NickName); - Assert.AreEqual("user", line.Hostmask.UserName); - Assert.AreEqual("host", line.Hostmask.HostName); - } - - [TestMethod] - public void TestEmptyHostmaskFromLine() - { - var line = new Line("PRIVMSG #channel hello"); - Assert.IsNull(line.Hostmask.HostName); - Assert.IsNull(line.Hostmask.UserName); - Assert.IsNull(line.Hostmask.NickName); - } - } -} diff --git a/IrcTokens/Tests/Parser.cs b/IrcTokens/Tests/Parser.cs deleted file mode 100644 index ed4e406..0000000 --- a/IrcTokens/Tests/Parser.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using IrcTokens.Tests.Data; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace IrcTokens.Tests -{ - [TestClass] - public class Parser - { - private static T LoadYaml(string path) - { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - return deserializer.Deserialize(File.ReadAllText(path)); - } - - [TestMethod] - public void TestSplit() - { - foreach (var test in LoadYaml("Tests/Data/msg-split.yaml").Tests) - { - var tokens = new Line(test.Input); - var atoms = test.Atoms; - - Assert.AreEqual(atoms.Verb.ToUpper(CultureInfo.InvariantCulture), tokens.Command, - $"command failed on: '{test.Input}'"); - Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'"); - CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'"); - CollectionAssert.AreEqual(atoms.Params ?? new List(), tokens.Params, - $"params failed on: '{test.Input}'"); - } - } - - [TestMethod] - public void TestJoin() - { - foreach (var test in LoadYaml("Tests/Data/msg-join.yaml").Tests) - { - var atoms = test.Atoms; - var line = new Line - { - Command = atoms.Verb, Params = atoms.Params, Source = atoms.Source, Tags = atoms.Tags - }.Format(); - - Assert.IsTrue(test.Matches.Contains(line), test.Description); - } - } - } -} diff --git a/IrcTokens/Tests/StatefulDecoder.cs b/IrcTokens/Tests/StatefulDecoder.cs deleted file mode 100644 index 9d2b8b7..0000000 --- a/IrcTokens/Tests/StatefulDecoder.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace IrcTokens.Tests -{ - [TestClass] - public class StatefulDecoder - { - private IrcTokens.StatefulDecoder _decoder; - - [TestInitialize] - public void TestInitialize() - { - _decoder = new IrcTokens.StatefulDecoder(); - } - - [TestMethod] - public void TestPartial() - { - var lines = _decoder.Push("PRIVMSG "); - Assert.AreEqual(0, lines.Count); - - lines = _decoder.Push("#channel hello\r\n"); - Assert.AreEqual(1, lines.Count); - - var line = new Line("PRIVMSG #channel hello"); - CollectionAssert.AreEqual(new List {line}, lines); - } - - [TestMethod] - public void TestMultiple() - { - var lines = _decoder.Push("PRIVMSG #channel1 hello\r\nPRIVMSG #channel2 hello\r\n"); - Assert.AreEqual(2, lines.Count); - - var line1 = new Line("PRIVMSG #channel1 hello"); - var line2 = new Line("PRIVMSG #channel2 hello"); - Assert.AreEqual(line1, lines[0]); - Assert.AreEqual(line2, lines[1]); - } - - [TestMethod] - public void TestEncoding() - { - var iso8859 = Encoding.GetEncoding("iso-8859-1"); - _decoder = new IrcTokens.StatefulDecoder {Encoding = iso8859}; - var bytes = iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"); - var lines = _decoder.Push(bytes, bytes.Length); - var line = new Line("PRIVMSG #channel :hello Ç"); - Assert.IsTrue(line.Equals(lines[0])); - } - - [TestMethod] - public void TestEncodingFallback() - { - var latin1 = Encoding.GetEncoding("iso-8859-1"); - _decoder = new IrcTokens.StatefulDecoder {Encoding = null, Fallback = latin1}; - var bytes = latin1.GetBytes("PRIVMSG #channel hélló\r\n"); - var lines = _decoder.Push(bytes, bytes.Length); - Assert.AreEqual(1, lines.Count); - Assert.IsTrue(new Line("PRIVMSG #channel hélló").Equals(lines[0])); - } - - [TestMethod] - public void TestEmpty() - { - var lines = _decoder.Push(string.Empty); - Assert.AreEqual(0, lines.Count); - } - - [TestMethod] - public void TestBufferUnfinished() - { - _decoder.Push("PRIVMSG #channel hello"); - var lines = _decoder.Push(string.Empty); - Assert.AreEqual(0, lines.Count); - } - - [TestMethod] - public void TestClear() - { - _decoder.Push("PRIVMSG "); - _decoder.Clear(); - Assert.AreEqual(string.Empty, _decoder.Pending); - } - } -} diff --git a/IrcTokens/Tests/StatefulEncoder.cs b/IrcTokens/Tests/StatefulEncoder.cs deleted file mode 100644 index f2cd6c4..0000000 --- a/IrcTokens/Tests/StatefulEncoder.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace IrcTokens.Tests -{ - [TestClass] - public class StatefulEncoder - { - private IrcTokens.StatefulEncoder _encoder; - - [TestInitialize] - public void TestInitialize() - { - _encoder = new IrcTokens.StatefulEncoder(); - } - - [TestMethod] - public void TestPush() - { - var line = new Line("PRIVMSG #channel hello"); - _encoder.Push(line); - Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending()); - } - - [TestMethod] - public void TestPopPartial() - { - var line = new Line("PRIVMSG #channel hello"); - _encoder.Push(line); - _encoder.Pop("PRIVMSG #channel hello".Length); - Assert.AreEqual("\r\n", _encoder.Pending()); - } - - [TestMethod] - public void TestPopReturned() - { - var line = new Line("PRIVMSG #channel hello"); - _encoder.Push(line); - _encoder.Push(line); - var lines = _encoder.Pop("PRIVMSG #channel hello\r\n".Length); - Assert.AreEqual(1, lines.Count); - Assert.AreEqual(line, lines[0]); - } - - [TestMethod] - public void TestPopNoneReturned() - { - var line = new Line("PRIVMSG #channel hello"); - _encoder.Push(line); - var lines = _encoder.Pop(1); - Assert.AreEqual(0, lines.Count); - } - - [TestMethod] - public void TestPopMultipleLines() - { - var line1 = new Line("PRIVMSG #channel1 hello"); - _encoder.Push(line1); - var line2 = new Line("PRIVMSG #channel2 hello"); - _encoder.Push(line2); - - var lines = _encoder.Pop(_encoder.Pending().Length); - Assert.AreEqual(2, lines.Count); - Assert.AreEqual(string.Empty, _encoder.Pending()); - } - - [TestMethod] - public void TestClear() - { - _encoder.Push(new Line("PRIVMSG #channel hello")); - _encoder.Clear(); - Assert.AreEqual(string.Empty, _encoder.Pending()); - } - - [TestMethod] - public void TestEncoding() - { - var iso8859 = Encoding.GetEncoding("iso-8859-1"); - _encoder = new IrcTokens.StatefulEncoder {Encoding = iso8859}; - _encoder.Push(new Line("PRIVMSG #channel :hello Ç")); - CollectionAssert.AreEqual(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"), _encoder.PendingBytes); - } - } -} diff --git a/IrcTokens/Tests/Tokenization.cs b/IrcTokens/Tests/Tokenization.cs deleted file mode 100644 index 7e2139d..0000000 --- a/IrcTokens/Tests/Tokenization.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace IrcTokens.Tests -{ - [TestClass] - public class Tokenization - { - [TestMethod] - public void TestTagsMissing() - { - var line = new Line("PRIVMSG #channel"); - Assert.IsNull(line.Tags); - } - - [TestMethod] - public void TestTagsMissingValue() - { - var line = new Line("@id= PRIVMSG #channel"); - Assert.AreEqual(string.Empty, line.Tags["id"]); - } - - [TestMethod] - public void TestTagsMissingEqual() - { - var line = new Line("@id PRIVMSG #channel"); - Assert.AreEqual(string.Empty, line.Tags["id"]); - } - - [TestMethod] - public void TestTagsUnescape() - { - var line = new Line(@"@id=1\\\:\r\n\s2 PRIVMSG #channel"); - Assert.AreEqual("1\\;\r\n 2", line.Tags["id"]); - } - - [TestMethod] - public void TestTagsOverlap() - { - var line = new Line(@"@id=1\\\s\\s PRIVMSG #channel"); - Assert.AreEqual("1\\ \\s", line.Tags["id"]); - } - - [TestMethod] - public void TestTagsLoneEndSlash() - { - var line = new Line("@id=1\\ PRIVMSG #channel"); - Assert.AreEqual("1", line.Tags["id"]); - } - - [TestMethod] - public void TestSourceWithoutTags() - { - var line = new Line(":nick!user@host PRIVMSG #channel"); - Assert.AreEqual("nick!user@host", line.Source); - } - - [TestMethod] - public void TestSourceWithTags() - { - var line = new Line("@id=123 :nick!user@host PRIVMSG #channel"); - Assert.AreEqual("nick!user@host", line.Source); - } - - [TestMethod] - public void TestSourceMissingWithoutTags() - { - var line = new Line("PRIVMSG #channel"); - Assert.IsNull(line.Source); - } - - [TestMethod] - public void TestSourceMissingWithTags() - { - var line = new Line("@id=123 PRIVMSG #channel"); - Assert.IsNull(line.Source); - } - - [TestMethod] - public void TestCommand() - { - var line = new Line("privmsg #channel"); - Assert.AreEqual("PRIVMSG", line.Command); - } - - [TestMethod] - public void TestParamsTrailing() - { - var line = new Line("PRIVMSG #channel :hello world"); - CollectionAssert.AreEqual(new List {"#channel", "hello world"}, line.Params); - } - - [TestMethod] - public void TestParamsOnlyTrailing() - { - var line = new Line("PRIVMSG :hello world"); - CollectionAssert.AreEqual(new List {"hello world"}, line.Params); - } - - [TestMethod] - public void TestParamsMissing() - { - var line = new Line("PRIVMSG"); - Assert.AreEqual("PRIVMSG", line.Command); - CollectionAssert.AreEqual(new List(), line.Params); - } - - [TestMethod] - public void TestAllTokens() - { - var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world"); - CollectionAssert.AreEqual(new Dictionary {{"id", "123"}}, line.Tags); - Assert.AreEqual("nick!user@host", line.Source); - Assert.AreEqual("PRIVMSG", line.Command); - CollectionAssert.AreEqual(new List {"#channel", "hello world"}, line.Params); - } - } -} diff --git a/README.md b/README.md index 1796da2..d012c78 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# IrcSharp +# IRCSharp [![Build Status](https://drone.tildegit.org/api/badges/ben/ircsharp/status.svg)](https://drone.tildegit.org/ben/ircsharp) -- cgit 1.4.1