using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; // ReSharper disable CommentTypo 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.ToUpperInvariant(); 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[1..].Split(';')) if (part.Contains('=', StringComparison.Ordinal)) { split = part.Split('=', 2); Tags[split[0]] = UnescapeTag(split[1]); } else { Tags[part] = null; } } 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][1..]; Params.RemoveAt(0); } if (Params.Count > 0) { Command = Params[0].ToUpperInvariant(); 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); } } }