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

                                 
                           






                                                       
                                        






                                                            
 




                                          
             
                                               

             
                               
             
                                             

             
                                               
             
                                                                 

             
                                           
             
                                                                                                     
             


                                                      
 



                                                                  
 
                                      
         

                              
                             
             
 





                                               
         











                                                                                                                                  
                                                
             
                                                              
             
 











                                                                         
                                                                     
                     
                                                   









                                                                        
                                                              
             
                                            
                                
                                    





                                

                                                                                 
                                            








                                                
                                                                          


























                                                                                                                         
             
                                       
             









                                                  
                                                                  
                     
                                                                                                  

                     
                                          
                     
                                                                                                       
                     


                                      
                                                                                                                            
                 
                                      

                 






                                          
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);
        }
    }
}