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 { public Dictionary Tags { get; set; } public string Source { get; set; } public string Command { get; set; } public List Params { get; set; } private Hostmask _hostmask; private readonly string _rawLine; 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() => Format().GetHashCode(StringComparison.Ordinal); public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; return Format() == ((Line) obj).Format(); } 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)); _rawLine = 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); } } }