using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace IrcTokens { /// /// Tools to represent, parse, and format IRC lines /// public class Line : IEquatable { public Dictionary Tags { get; set; } public string Source { get; set; } public string Command { get; set; } public List Params { get; set; } private Hostmask _hostmask; 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 bool Equals(Line other) { if (other == null) { return false; } return Format() == other.Format(); } public override bool Equals(object obj) { return Equals(obj as Line); } public Hostmask Hostmask => _hostmask ??= new Hostmask(Source); public Line() { } /// /// 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(" "); var messageTags = split[0]; line = string.Join(" ", split.Skip(1)); foreach (var part in messageTags.Substring(1).Split(';')) { if (part.Contains('=', StringComparison.Ordinal)) { split = part.Split('=', 2); Tags[split[0]] = Protocol.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); } } /// /// 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 .Select(key => string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={Protocol.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]; Params.RemoveAt(Params.Count - 1); foreach (var p in Params) { 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(Params); if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) || last.StartsWith(':')) { last = $":{last}"; } outs.Add(last); } return string.Join(" ", outs); } } }