about summary refs log tree commit diff
path: root/IRCTokens/Line.cs
diff options
context:
space:
mode:
authorBen Harris <ben@tilde.team>2020-05-14 23:06:10 -0400
committerBen Harris <ben@tilde.team>2020-05-14 23:17:47 -0400
commit21f1e95fb8e935134a969bc3d729964d8d2aadfa (patch)
treedb2be27e9b5ac48e19f92b56cbad68ab59f7099e /IRCTokens/Line.cs
parent304df7805b9925c2edd992fd4177eef80197f807 (diff)
rename Irc to IRC
Diffstat (limited to 'IRCTokens/Line.cs')
-rw-r--r--IRCTokens/Line.cs233
1 files changed, 233 insertions, 0 deletions
diff --git a/IRCTokens/Line.cs b/IRCTokens/Line.cs
new file mode 100644
index 0000000..bf3cc91
--- /dev/null
+++ b/IRCTokens/Line.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace IRCTokens
+{
+    /// <summary>
+    ///     Tools to represent, parse, and format IRC lines
+    /// </summary>
+    public class Line : IEquatable<Line>
+    {
+        private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"};
+
+        private static readonly string[] TagEscaped = {"\\\\", "\\s", "\\:", "\\r", "\\n"};
+
+        private Hostmask _hostmask;
+
+        public Line()
+        {
+        }
+
+        public Line(string command, params string[] parameters)
+        {
+            Command = command;
+            Params  = parameters.ToList();
+        }
+
+        /// <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(" ", 2);
+                var messageTags = split[0];
+                line = split[1];
+
+                foreach (var part in messageTags.Substring(1).Split(';'))
+                    if (part.Contains('=', StringComparison.Ordinal))
+                    {
+                        split          = part.Split('=', 2);
+                        Tags[split[0]] = 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);
+        }
+
+        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>
+        private static string UnescapeTag(string val)
+        {
+            var unescaped = new StringBuilder();
+
+            var graphemeIterator = StringInfo.GetTextElementEnumerator(val);
+            graphemeIterator.Reset();
+
+            while (graphemeIterator.MoveNext())
+            {
+                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);
+            }
+
+            return unescaped.ToString();
+        }
+
+        /// <summary>
+        ///     Escape strings for use in ircv3 tags
+        /// </summary>
+        /// <param name="val">string to escape</param>
+        /// <returns>escaped string</returns>
+        private static string EscapeTag(string val)
+        {
+            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
+        /// </summary>
+        /// <returns>formatted irc line</returns>
+        public string Format()
+        {
+            var outs = new List<string>();
+
+            if (Tags != null && Tags.Any())
+            {
+                var tags = Tags.Keys
+                    .OrderBy(k => k)
+                    .Select(key =>
+                        string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={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];
+                var withoutLast = Params.SkipLast(1).ToList();
+
+                foreach (var p in withoutLast)
+                {
+                    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(withoutLast);
+
+                if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) ||
+                    last.StartsWith(':'))
+                    last = $":{last}";
+
+                outs.Add(last);
+            }
+
+            return string.Join(" ", outs);
+        }
+    }
+}