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 --- IRCTokens/Line.cs | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 IRCTokens/Line.cs (limited to 'IRCTokens/Line.cs') 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); + } + } +} -- cgit 1.4.1