Quantcast
Channel: Alxandr.me » c#
Viewing all articles
Browse latest Browse all 11

dotRant: Making an IRC library for .NET in C# – Part 5: Reflection and IRCCommands

$
0
0

Last post we discussed methods of responding to messages coming from the IRC-server, and I also explained the method I wanted to use, and why I would want to use it. Today I will do the actual implementation of the described method. Also, to handle IRC commands, we need to parse them, and we need a method of sending replies to the server, and last but not least we need to create the attribute to be set on the methods that we want called. But lets start with the first thing first:

Parsing IRC-commands

As discuss earlier, an IRC-command consists of mainly 3 parts, a prefix, a name, and a list of parameters. Before we can do anything we need to split the command up into these three parts so that we know what command is actually send, and by whom, and we need to create a data-structure to hold this data. For this we will create the IRCCommand class.

    /// <summary>
    /// A class used to represent IRC commands.
    /// </summary>
    [DebuggerDisplay("{ToString()}", Name = "{Name}")]
    public class IrcCommand
    {
        private string name;
        private string prefix;
        private IrcParameter[] parameters;

        /// <summary>
        /// Initializes a new instance of the <see cref="IrcCommand"/> class.
        /// </summary>
        /// <param name="prefix">The prefix.</param>
        /// <param name="name">The name.</param>
        /// <param name="parameters">The parameters.</param>
        public IrcCommand(string prefix, string name, params IrcParameter[] parameters)
        {
            this.prefix = prefix;
            this.name = name;
            this.parameters = parameters;

            var restParams = parameters.Where(p => p.IsRest);
            if (restParams.Count() > 1)
                throw new ArgumentException("Can't be more than 1 rest-parameter.", "parameters");
            else if (restParams.Count() == 1 && restParams.First() != parameters.Last())
                throw new ArgumentException("Rest parameter must always be the last parameter.", "parameters");
        }

        /// <summary>
        /// Gets the name.
        /// </summary>
        public string Name
        {
            get { return name; }
        }

        /// <summary>
        /// Gets the prefix.
        /// </summary>
        public string Prefix
        {
            get { return prefix; }
        }

        /// <summary>
        /// Gets the parameters.
        /// </summary>
        public string[] Parameters
        {
            get { return parameters.Select(p => (string)p).ToArray(); }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return (prefix == null ? "" : ":" + prefix + " ") +
                name + " " + String.Join(" ", parameters.Select(p => p.ToString()));
        }
    }

    /// <summary>
    /// Represent an IRC-parameter.
    /// </summary>
    [DebuggerDisplay("{ToString()}")]
    public class IrcParameter
    {
        string value;
        bool isRest;

        /// <summary>
        /// Initializes a new instance of the <see cref="IrcParameter"/> class.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="isRest">if set to <c>true</c> [is rest].</param>
        public IrcParameter(string value, bool isRest = false)
        {
            this.value = value;
            this.isRest = isRest;

            if (!isRest && value.IndexOfAny(new char[] { '\0', '\n', '\r', ' ' }) != -1)
                throw new ArgumentException("value can't contain '\\0', '\\n', '\\r' or ' ' in a non-rest-parameter.", "value");
        }

        /// <summary>
        /// Gets the value.
        /// </summary>
        public string Value
        {
            get { return value; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is rest-value.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is rest-value; otherwise, <c>false</c>.
        /// </value>
        public bool IsRest
        {
            get { return isRest; }
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="System.String"/> to <see cref="dotRant.IrcParameter"/>.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>
        /// The result of the conversion.
        /// </returns>
        public static implicit operator IrcParameter(string parameter)
        {
            return new IrcParameter(parameter);
        }

        /// <summary>
        /// Performs an explicit conversion from <see cref="dotRant.IrcParameter"/> to <see cref="System.String"/>.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>
        /// The result of the conversion.
        /// </returns>
        public static explicit operator string(IrcParameter parameter)
        {
            return parameter.value;
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return (isRest ? ":" : "") + value;
        }
    }

These are two fairly simple classes which don’t do much more than hold the data required, also, the IrcParameter contains conversion to and from string using implicit and explicit operators. Also, the constructor for both classes performs checks as to the validity of the data given. The only thing left to do here is to add parsing of the commands, but before we do that, let’s take a look at how the commands are defined in the IRC specification (RFC 2812):
PS: Please do note that what I here refer to as a command, is referred to as a message in this specification.

    message    =  [ ":" prefix SPACE ] command [ params ] crlf
    prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
    command    =  1*letter / 3digit
    params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
               =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

    nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                    ; any octet except NUL, CR, LF, " " and ":"
    middle     =  nospcrlfcl *( ":" / nospcrlfcl )
    trailing   =  *( ":" / " " / nospcrlfcl )

    SPACE      =  %x20        ; space character
    crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

Normally, IRC-commands are really simple. They’ve split by space and have a prefix, however, when reading the specification it becomes clear that they can be much more complex looking then that. For instance, parameters that are not “trailing” (or as I’ve called them, rest-parameters) can contain ‘:’, they just can’t start with a colon. This means we can’t just simply split on ‘:’ to get rest-parameters, neither can we simply split on space, cause spaces needs to be maintained in the rest-parameter. Nevertheless, it’s not a very complex thing to parse, as you will soon see.

A thing to notice first, is that you can have messages without a prefix, and that if there is a prefix, the command should start with a colon. This can be checked easily by doing something like this:

        /// <summary>
        /// Parses the specified command.
        /// </summary>
        /// <param name="cmd">The command.</param>
        /// <returns>The IrcCommand-representation of the given command.</returns>
        public static IrcCommand Parse(string cmd)
        {
            string prefix = null, name = null;
            List<IrcParameter> parameters = new List<IrcParameter>();
            if (cmd[0] == ':')
            {
                prefix = cmd.Substring(1, cmd.IndexOf(' ') - 1);
                cmd = cmd.Substring(cmd.IndexOf(' ') + 1);
            }

After that, we need to get the name of the command, which too is really simple, and can be achieved with the following 3 lines:

            var nameEnd = cmd.IndexOf(' ');
            name = cmd.Substring(0, nameEnd);
            cmd = cmd.Substring(nameEnd + 1);

Then, what’s left is the parameters, and the rules for parameters is that if they start with a space, it’s a rest-parameter, and there can only be one of them, thus the code can look like this:

            while (cmd.Length > 0)
            {
                if (cmd[0] == ':')
                {
                    parameters.Add(cmd.Substring(1).AsIrcRestParameter());
                    cmd = "";
                }
                else
                {
                    var paramEnd = cmd.IndexOf(' ');
                    if (paramEnd == -1)
                        paramEnd = cmd.Length;
                    var param = cmd.Substring(0, paramEnd);
                    parameters.Add(param);
                    if (paramEnd != cmd.Length)
                        cmd = cmd.Substring(paramEnd + 1);
                    else
                        cmd = "";
                }
            }

            return new IrcCommand(prefix, name, parameters.ToArray());
        }

For the following code to work we need to add an extension-method to System.String that turns it into an IRC rest-parameter. I’ve added 2 excension-methods to help with this in the following helper-class:

    /// <summary>
    /// Provides extension-helpers for IrcParameters.
    /// </summary>
    public static class IrcParameterExtensions
    {
        /// <summary>
        /// Converts the string to an irc parameter.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <param name="isRest">if set to <c>true</c> treated as rest-parameter, if <c>false</c> not treated as rest-parameter, and if <c>null</c> selects automatically.</param>
        /// <returns>A irc parameter.</returns>
        public static IrcParameter AsIrcParameter(this string parameter, bool? isRest = null)
        {
            return new IrcParameter(parameter, isRest.HasValue ? isRest.Value : parameter.IndexOfAny(new char[] { '\0', '\n', '\r', ' ', ':' }) != -1);
        }

        /// <summary>
        /// Converts the string to an irc parameter.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>A irc rest-parameter.</returns>
        public static IrcParameter AsIrcRestParameter(this string parameter)
        {
            return new IrcParameter(parameter, true);
        }
    }

And there you have it. That’s all that’s required for parsing IRC-commands. As said, IRC is a rather simple protocol. Now we got more or less everything we need to start responding to commands that are sent from the server.

Handling commands

In order to actually handle commands sent from the server it’s needed to tell our class what functions it should trigger for what commands, and in order to do that I decided to use attributes as explained in the previous post. But in order to do so, we first need to actually create the attribute-class. All attribute-classes needs to inherit from Attribute, and they can have an AttributeUsageAttribute to denote where and how you can use the attribute. The attribute we are to create is fairly simple. All it requires is a name and the ability to later retrieve that name, and can be written like this:

    /// <summary>
    /// Attribute used to denote IrcCommand-methods
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class IrcCommandAttribute : Attribute
    {
        private string name;
        /// <summary>
        /// Initializes a new instance of the <see cref="IrcCommandAttribute"/> class with a name.
        /// </summary>
        /// <param name="name">The name.</param>
        public IrcCommandAttribute(string name)
        {
            this.name = name;
        }

        /// <summary>
        /// Gets the name.
        /// </summary>
        public string Name
        {
            get { return name; }
        }
    }

Now we’re all set. Let’s start by updating the client_Message method in our IrcClient class to look like this:

        void client_Message(object sender, MessageEventArgs e)
        {
            string msg = encoding.GetString(e.Message);
            var cmd = IrcCommand.Parse(msg);
            GetCommand(cmd.Name).Invoke(this, new object[] { cmd.Prefix, cmd.Name, cmd.Parameters });
        }

Pretty slick, eh? Anyways, with this added we need to create the GetCommand(string name) method, but first I’d like to talk a bit about the strategy we take here. As explained in the previous post I aim to use the DefaultDictionary to store references from name to function-call and thus we need to populate a DefaultDictionary somewhere. However, consider a scenario where you have more than a 100 IrcClient instances running at once. There is no need for each of them to have their own DefaultDictionary cause they all have the same methods, thus we can make it static. This saves both memory, and initialization-time of our class since we only need to build the dictionary once. However, there is one problem with this approach too, and that is if you subclass the IrcClient. If you do so you can get various funny effects like the application not doing what you want it to, or simply just crashing. To prevent this we need a double-dictionary that maintains a DefaultDictionary for each type of IrcClient we currently have. Like this:

private static readonly Dictionary<Type, DefaultDictionary<string, MethodInfo>> ircCommands = new Dictionary<Type, DefaultDictionary<string, MethodInfo>>();

The rest is pretty straight forward. When you call the GetCommand(string name) method it calls a static GetCommand(Type type, string name) method that checks if the type is present in the first dictionary. If it’s not it creates a new DefaultDictionary for that type. After that it returns the MethodInfo for the method to call regardless if the DefaultDictionary was just created or not. To create the DefaultDictionary I employ a simple LINQ-trick and all in all we end up with this:

        private static MethodInfo GetCommand(Type type, string cmdName)
        {
            if (!ircCommands.ContainsKey(type))
            {
                lock (ircCommands)
                {
                    if (!ircCommands.ContainsKey(type))
                    {
                        var dict = new DefaultDictionary<string, MethodInfo>();
                        var methods = from mi in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod)
                                      from attr in mi.GetCustomAttributes(typeof(IrcCommandAttribute), true).Cast<IrcCommandAttribute>()
                                      select new { Method = mi, Name = attr.Name };
                        foreach (var m in methods)
                        {
                            if (m.Name != "_Default")
                                dict[m.Name] = m.Method;
                            else
                                dict.Default = m.Method;
                        }
                        ircCommands.Add(type, dict);
                    }
                }
            }
            return ircCommands[type][cmdName];
        }
        private MethodInfo GetCommand(string cmdName) { return GetCommand(this.GetType(), cmdName); }

After that we can go ahead and add a couple of methods to our class. Just to clarify things I prefix these methods with IrcCmd_.
I know these comments makes no sence, but I didn’t notice at the time of writing the code. The comments are auto-generated.

        /// <summary>
        /// Ircs the CMD_ default.
        /// </summary>
        /// <param name="prefix">The prefix.</param>
        /// <param name="cmd">The CMD.</param>
        /// <param name="parameters">The parameters.</param>
        [IrcCommand("_Default")]
        protected virtual void IrcCmd_Default(string prefix, string cmd, string[] parameters)
        {
            //TODO: Create and fire an event.
        }

        /// <summary>
        /// Ircs the CMD_ ping.
        /// </summary>
        /// <param name="prefix">The prefix.</param>
        /// <param name="cmd">The CMD.</param>
        /// <param name="parameters">The parameters.</param>
        [IrcCommand("PING")]
        protected virtual void IrcCmd_Ping(string prefix, string cmd, string[] parameters)
        {
            Send(new IrcCommand(null, "PONG", parameters[0].AsIrcParameter()));
        }

Small changes

I’ve changed a few things about the IrcClient class since my last post, and the main thing that’s changed is the constructor. I removed the parameters from the constructor and rather made them into properties instead, so the constructor now looks like this:

        /// <summary>
        /// Initializes a new instance of the <see cref="IrcClient"/> class.
        /// </summary>
        public IrcClient()
        {
            if (encoding == null)
                encoding = Encoding.UTF8;

            client = new MessageConnection();
            client.Disconnect += new EventHandler<EventArgs>(client_Disconnect);
            client.SslValidate += new EventHandler<SslValidateEventArgs>(client_SslValidate);
            client.Message += new EventHandler<MessageEventArgs>(client_Message);
        }

And also I’ve added 3 new properties that looks like this:

        /// <summary>
        /// Gets or sets the encoding.
        /// </summary>
        /// <value>
        /// The encoding.
        /// </value>
        public Encoding Encoding
        {
            get
            {
                return encoding;
            }
            set
            {
                encoding = value;
            }
        }

        /// <summary>
        /// Gets or sets the nick.
        /// </summary>
        /// <value>
        /// The nick.
        /// </value>
        public string Nick
        {
            get
            {
                return nick;
            }
            set
            {
                nick = value;
                if (Connected)
                    Send(new IrcCommand(null, "NICK", nick));
            }
        }

        /// <summary>
        /// Gets or sets the full name.
        /// </summary>
        /// <value>
        /// The full name.
        /// </value>
        public string FullName
        {
            get
            {
                return fullName;
            }
            set
            {
                if (Connected)
                    throw new InvalidOperationException("Can't change full-name after connected.");
                fullName = value;
            }
        }

Then there’s the ConnectAsync method that now looks like this:

        public Task ConnectAsync(string hostname, int port, bool secureConnection)
        {
            if (String.IsNullOrWhiteSpace(nick))
                throw new InvalidOperationException("Can't connect with nick beeing unset.");
            if (String.IsNullOrWhiteSpace(fullName))
                throw new InvalidOperationException("Can't connect with fullname beeing unset.");
            return client.ConnectAsync(hostname, port, secureConnection).ContinueWith(task =>
            {
                Send(new IrcCommand(null, "PASS", "*"));
                Send(new IrcCommand(null, "NICK", nick));
                Send(new IrcCommand(null, "USER", "dotRant", "8", "*", fullName.AsIrcRestParameter()));
                // PASS *
                // NICK <nick>
                // USER dotRant 8 * :<full name>
            });
        }

And last but not least the Send-method:

        private void Send(IrcCommand cmd)
        {
            var bytes = encoding.GetBytes(cmd.ToString());
            client.Send(bytes);
        }

As always, if there are any questions, please leave a comment below. The code for this post can be found here: https://github.com/Alxandr/dotRant—Old-Edition/tree/part-5/src/dotRant.

Until then. Alxandr.



Viewing all articles
Browse latest Browse all 11

Trending Articles