// ReSharper disable ReplaceWithFieldKeyword // ReSharper disable CommentTypo namespace IRCTokens; /// Tools to represent, parse, and format IRC lines public class Line : IEquatable { private Hostmask _hostmask; public Line() { } public Line(string command, params string[] parameters) { Command = command.ToUpperInvariant(); Params = parameters.ToList(); } /// Parse a 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 = []; split = line.Split([' '], 2); var messageTags = split[0]; line = split[1]; foreach (var part in messageTags.Substring(1).Split(';')) { if (part.Contains('=')) { split = part.Split(['='], 2); Tags[split[0]] = TagEscape.Unescape(split[1]); } else { Tags[part] = null; } } } string trailing; if (line.Contains(" :")) { split = line.Split([" :"], 2, StringSplitOptions.None); line = split[0]; trailing = split[1]; } else { trailing = null; } Params = line.Contains(' ') ? [.. line.Split([" "], StringSplitOptions.RemoveEmptyEntries)] : [line]; if (Params[0].StartsWith(":")) { Source = Params[0].Substring(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(Source); public bool Equals(Line other) { if (other == null) { return false; } return Format() == other.Format(); } public override string ToString() { List vars = []; 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() => Format().GetHashCode(); public override bool Equals(object obj) => Equals(obj as Line); /// Format a as a standards-compliant IRC line /// formatted irc line public string Format() { List outs = []; if (Tags != null && Tags.Any()) { var tags = Tags.Keys .OrderBy(k => k) .Select(key => string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={TagEscape.Escape(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.Last(); var withoutLast = Params.SkipLast(1).ToList(); foreach (var p in withoutLast) { if (p.Contains(' ')) { 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(' ') || last.StartsWith(":")) { last = $":{last}"; } outs.Add(last); } return string.Join(" ", outs); } }