ircsharp/IRCTokens/Line.cs

234 lines
7.1 KiB
C#
Raw Normal View History

2020-04-20 00:52:41 +00:00
using System;
using System.Collections.Generic;
using System.Globalization;
2020-04-20 00:52:41 +00:00
using System.Linq;
2020-04-28 04:35:52 +00:00
using System.Text;
2020-04-20 00:52:41 +00:00
2020-05-15 03:06:10 +00:00
namespace IRCTokens
2020-04-20 00:52:41 +00:00
{
/// <summary>
2020-04-28 04:35:52 +00:00
/// Tools to represent, parse, and format IRC lines
2020-04-20 00:52:41 +00:00
/// </summary>
public class Line : IEquatable<Line>
2020-04-20 00:52:41 +00:00
{
2020-04-28 04:35:52 +00:00
private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"};
2020-04-20 21:24:59 +00:00
2023-11-07 22:54:58 +00:00
private static readonly string[] TagEscaped = {@"\\", "\\s", "\\:", "\\r", "\\n"};
2020-04-20 21:24:59 +00:00
2020-04-28 04:35:52 +00:00
private Hostmask _hostmask;
2020-04-28 04:35:52 +00:00
public Line()
{
2020-04-20 21:24:59 +00:00
}
2020-04-20 00:52:41 +00:00
2020-05-06 14:39:59 +00:00
public Line(string command, params string[] parameters)
{
Command = command;
Params = parameters.ToList();
}
2020-04-20 00:52:41 +00:00
/// <summary>
2020-04-28 04:35:52 +00:00
/// Build new <see cref="Line" /> object parsed from
/// <param name="line">a string</param>
/// . Analogous to irctokens.tokenise()
2020-04-20 00:52:41 +00:00
/// </summary>
/// <param name="line">irc line to parse</param>
public Line(string line)
{
2020-04-28 04:35:52 +00:00
if (string.IsNullOrWhiteSpace(line)) throw new ArgumentNullException(nameof(line));
2020-04-20 00:52:41 +00:00
string[] split;
if (line.StartsWith('@'))
{
Tags = new Dictionary<string, string>();
2020-05-06 14:45:37 +00:00
split = line.Split(" ", 2);
2020-04-20 00:52:41 +00:00
var messageTags = split[0];
2020-05-06 14:45:37 +00:00
line = split[1];
2020-04-20 00:52:41 +00:00
2023-11-07 22:54:58 +00:00
foreach (var part in messageTags[1..].Split(';'))
if (part.Contains('=', StringComparison.Ordinal))
2020-04-20 00:52:41 +00:00
{
2020-04-28 04:35:52 +00:00
split = part.Split('=', 2);
Tags[split[0]] = UnescapeTag(split[1]);
2020-04-20 00:52:41 +00:00
}
else
{
Tags[part] = string.Empty;
}
}
string trailing;
if (line.Contains(" :", StringComparison.Ordinal))
2020-04-20 00:52:41 +00:00
{
2020-04-28 04:35:52 +00:00
split = line.Split(" :", 2);
line = split[0];
trailing = split[1];
2020-04-20 00:52:41 +00:00
}
else
{
trailing = null;
}
Params = line.Contains(' ', StringComparison.Ordinal)
? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
2020-04-28 04:35:52 +00:00
: new List<string> {line};
2020-04-20 00:52:41 +00:00
if (Params[0].StartsWith(':'))
{
2023-11-07 22:54:58 +00:00
Source = Params[0][1..];
2020-04-20 00:52:41 +00:00
Params.RemoveAt(0);
}
if (Params.Count > 0)
{
Command = Params[0].ToUpper(CultureInfo.InvariantCulture);
2020-04-20 00:52:41 +00:00
Params.RemoveAt(0);
}
2020-04-28 04:35:52 +00:00
if (trailing != null) Params.Add(trailing);
}
public Dictionary<string, string> Tags { get; set; }
public string Source { get; set; }
public string Command { get; set; }
public List<string> Params { get; set; }
public Hostmask Hostmask =>
_hostmask ??= new Hostmask(Source);
public bool Equals(Line other)
{
if (other == null) return false;
return Format() == other.Format();
}
/// <summary>
/// Unescape ircv3 tag
/// </summary>
/// <param name="val">escaped string</param>
/// <returns>unescaped string</returns>
2020-04-28 15:08:01 +00:00
private static string UnescapeTag(string val)
2020-04-28 04:35:52 +00:00
{
var unescaped = new StringBuilder();
var graphemeIterator = StringInfo.GetTextElementEnumerator(val);
graphemeIterator.Reset();
while (graphemeIterator.MoveNext())
2020-04-20 00:52:41 +00:00
{
2020-04-28 04:35:52 +00:00
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);
2020-04-20 00:52:41 +00:00
}
2020-04-28 04:35:52 +00:00
return unescaped.ToString();
2020-04-20 00:52:41 +00:00
}
/// <summary>
2020-04-28 04:35:52 +00:00
/// Escape strings for use in ircv3 tags
/// </summary>
/// <param name="val">string to escape</param>
/// <returns>escaped string</returns>
2020-04-28 15:08:01 +00:00
private static string EscapeTag(string val)
2020-04-28 04:35:52 +00:00
{
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<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 override bool Equals(object obj)
{
return Equals(obj as Line);
}
/// <summary>
/// Format a <see cref="Line" /> as a standards-compliant IRC line
2020-04-20 00:52:41 +00:00
/// </summary>
/// <returns>formatted irc line</returns>
public string Format()
{
var outs = new List<string>();
if (Tags != null && Tags.Any())
{
var tags = Tags.Keys
2020-04-28 04:35:52 +00:00
.OrderBy(k => k)
.Select(key =>
string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={EscapeTag(Tags[key])}")
2020-04-20 00:52:41 +00:00
.ToList();
outs.Add($"@{string.Join(";", tags)}");
}
2020-04-28 04:35:52 +00:00
if (Source != null) outs.Add($":{Source}");
2020-04-20 00:52:41 +00:00
outs.Add(Command);
if (Params != null && Params.Any())
{
var last = Params[^1];
2020-04-28 05:52:24 +00:00
var withoutLast = Params.SkipLast(1).ToList();
2020-04-20 00:52:41 +00:00
2020-04-28 05:52:24 +00:00
foreach (var p in withoutLast)
2020-04-20 00:52:41 +00:00
{
if (p.Contains(' ', StringComparison.Ordinal))
2024-03-26 20:10:54 +00:00
throw new ArgumentException("non-last parameters cannot have spaces", p);
2020-04-20 00:52:41 +00:00
if (p.StartsWith(':'))
2024-03-26 20:10:54 +00:00
throw new ArgumentException("non-last parameters cannot start with colon", p);
2020-04-20 00:52:41 +00:00
}
2020-04-28 04:35:52 +00:00
2020-04-28 05:52:24 +00:00
outs.AddRange(withoutLast);
2020-04-20 00:52:41 +00:00
2020-04-28 04:35:52 +00:00
if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) ||
last.StartsWith(':'))
2020-04-20 00:52:41 +00:00
last = $":{last}";
2020-04-20 00:52:41 +00:00
outs.Add(last);
}
return string.Join(" ", outs);
}
}
}