about summary refs log blame commit diff
path: root/IrcTokens/Line.cs
blob: d1a8286be8fbac89a07a5c2a29cee0a3888fa4d6 (plain) (tree)
1
2
3
4
5
6
7
8
9

                                 
                           
                  
                  



                   
                                                           
                  
                                        
     
                                                                                     
 
                                                                                           
 
                                   
 
                     
         
         
 
                     


                                                                



                                                        
                                                                                               
 










                                                                         
                                                                     
                     

                                                               




                                                  


                            
                                                              
             

                                               
                                    





                                

                                                                                 
                                          








                                                
                                                                          


                                   






















                                                            
                                                     






                                                                            
             

















                                                                           
             

                                        


                     



                                                      
                                                   


































                                                                                                     








                                                 


                                                                                                     




                                                       
                                                       





                                               
                                                              
 
                                              
                 

                                                                                                  
 
                                          
                                                                                                       
                 
 
                                           
 

                                                                                                      
                                      
 






                                          
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()
        {
        }

        /// <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]] = 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);
        }
    }
}