ircsharp/IrcTokens/Line.cs

196 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace IrcTokens
{
/// <summary>
/// Tools to represent, parse, and format IRC lines
/// </summary>
public class Line : IEquatable<Line>
{
public Dictionary<string, string> Tags { get; set; }
public string Source { get; set; }
public string Command { get; set; }
public List<string> Params { get; set; }
private Hostmask _hostmask;
public override string ToString()
{
var vars = new List<string>();
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() { }
/// <summary>
/// Build new <see cref="Line"/> object parsed from <param name="line">a string</param>. Analogous to irctokens.tokenise()
/// </summary>
/// <param name="line">irc line to parse</param>
public Line(string line)
{
if (string.IsNullOrWhiteSpace(line))
{
throw new ArgumentNullException(nameof(line));
}
string[] split;
if (line.StartsWith('@'))
{
Tags = new Dictionary<string, string>();
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<string> { 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);
}
}
/// <summary>
/// Format a <see cref="Line"/> as a standards-compliant IRC line
/// </summary>
/// <returns>formatted irc line</returns>
public string Format()
{
var outs = new List<string>();
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);
}
}
}