~sircmpwn/ChatSharp

d1424ba162141a73397945dc867076d39d15dd90 — Drew DeVault 5 years ago ab980ae + 545896b
Merge pull request #86 from RockyTV/caps

Support WHOX queries
A ChatSharp/Events/WhoEventArgs.cs => ChatSharp/Events/WhoEventArgs.cs +21 -0
@@ 0,0 1,21 @@
using System;

namespace ChatSharp.Events
{
    /// <summary>
    /// Describes the response to a WHO (WHOX protocol) query. Note that ChatSharp may generate WHO
    /// queries that the user did not ask for.
    /// </summary>
    public class WhoxReceivedEventArgs : EventArgs
    {
        /// <summary>
        /// The WHOIS response from the server.
        /// </summary>
        public ExtendedWho[] WhoxResponse { get; set; }

        internal WhoxReceivedEventArgs(ExtendedWho[] whoxResponse)
        {
            WhoxResponse = whoxResponse;
        }
    }
}

M ChatSharp/Handlers/MessageHandlers.cs => ChatSharp/Handlers/MessageHandlers.cs +2 -0
@@ 41,10 41,12 @@ namespace ChatSharp.Handlers
            client.SetHandler("311", UserHandlers.HandleWhoIsUser);
            client.SetHandler("312", UserHandlers.HandleWhoIsServer);
            client.SetHandler("313", UserHandlers.HandleWhoIsOperator);
            client.SetHandler("315", UserHandlers.HandleWhoEnd);
            client.SetHandler("317", UserHandlers.HandleWhoIsIdle);
            client.SetHandler("318", UserHandlers.HandleWhoIsEnd);
            client.SetHandler("319", UserHandlers.HandleWhoIsChannels);
            client.SetHandler("330", UserHandlers.HandleWhoIsLoggedInAs);
            client.SetHandler("354", UserHandlers.HandleWhox);

            // Listing handlers
            client.SetHandler("367", ListingHandlers.HandleBanListPart);

M ChatSharp/Handlers/ServerHandlers.cs => ChatSharp/Handlers/ServerHandlers.cs +9 -0
@@ 86,6 86,15 @@ namespace ChatSharp.Handlers
                            break;
                    }
                }
                else
                {
                    switch (key.ToUpper())
                    {
                        case "WHOX":
                            client.ServerInfo.ExtendedWho = true;
                            break;
                    }
                }
            }
            client.OnServerInfoRecieved(new SupportsEventArgs(client.ServerInfo));
        }

M ChatSharp/Handlers/UserHandlers.cs => ChatSharp/Handlers/UserHandlers.cs +128 -0
@@ 1,5 1,6 @@
using System;
using System.Linq;
using System.Collections.Generic;

namespace ChatSharp.Handlers
{


@@ 70,5 71,132 @@ namespace ChatSharp.Handlers
                request.Callback(request);
            client.OnWhoIsReceived(new Events.WhoIsReceivedEventArgs(whois));
        }

        public static void HandleWhox(IrcClient client, IrcMessage message)
        {
            int msgQueryType = int.Parse(message.Parameters[1]);
            var whoxQuery = new KeyValuePair<string, RequestOperation>();

            foreach (var query in client.RequestManager.PendingOperations.Where(kvp => kvp.Key.StartsWith("WHO ")))
            {
                // Parse query to retrieve querytype
                string key = query.Key;
                string[] queryParts = key.Split(new[] { ' ' });

                int queryType = int.Parse(queryParts[2]);

                // Check querytype against message querytype
                if (queryType == msgQueryType) whoxQuery = query;
            }

            if (whoxQuery.Key != string.Empty && whoxQuery.Value != null)
            {
                var whoxList = (List<ExtendedWho>)client.RequestManager.PeekOperation(whoxQuery.Key).State;
                var whox = new ExtendedWho();

                string key = whoxQuery.Key;
                string[] queryParts = key.Split(new[] { ' ' });

                // Handle what fields were included in the WHOX request
                WhoxField fields = (WhoxField)int.Parse(queryParts[3]);
                int fieldIdx = 1;
                do
                {
                    if ((fields & WhoxField.QueryType) != 0)
                    {
                        whox.QueryType = msgQueryType;
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Channel) != 0)
                    {
                        whox.Channel = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Username) != 0)
                    {
                        whox.User.User = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.UserIp) != 0)
                    {
                        whox.IP = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Hostname) != 0)
                    {
                        whox.User.Hostname = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.ServerName) != 0)
                    {
                        whox.Server = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Nick) != 0)
                    {
                        whox.User.Nick = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Flags) != 0)
                    {
                        whox.Flags = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.Hops) != 0)
                    {
                        whox.Hops = int.Parse(message.Parameters[fieldIdx]);
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.TimeIdle) != 0)
                    {
                        whox.TimeIdle = int.Parse(message.Parameters[fieldIdx]);
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.AccountName) != 0)
                    {
                        whox.Account = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.OpLevel) != 0)
                    {
                        whox.OpLevel = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }

                    if ((fields & WhoxField.RealName) != 0)
                    {
                        whox.User.RealName = message.Parameters[fieldIdx];
                        fieldIdx++;
                    }
                }
                while (fieldIdx < message.Parameters.Length - 1);
                whoxList.Add(whox);
            }
        }

        public static void HandleWhoEnd(IrcClient client, IrcMessage message)
        {
            var query = client.RequestManager.PendingOperations.Where(kvp => kvp.Key.StartsWith("WHO " + message.Parameters[1])).FirstOrDefault();
            var request = client.RequestManager.DequeueOperation(query.Key);
            var whoxList = (List<ExtendedWho>)request.State;

            foreach (var whox in whoxList)
                if (!client.Users.Contains(whox.User.Nick))
                    client.Users.Add(whox.User);

            request.Callback?.Invoke(request);
            client.OnWhoxReceived(new Events.WhoxReceivedEventArgs(whoxList.ToArray()));
        }
    }
}

M ChatSharp/IrcClient.Commands.cs => ChatSharp/IrcClient.Commands.cs +30 -0
@@ 1,4 1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace ChatSharp


@@ 146,6 147,35 @@ namespace ChatSharp
        }

        /// <summary>
        /// Sends an extended WHO query asking for specific information about a single user
        /// or the users in a channel, and runs a callback when we have received the response.
        /// </summary>
        public void Who(string target, WhoxFlag flags, WhoxField fields, Action<ExtendedWho> callback)
        {
            if (ServerInfo.ExtendedWho)
            {
                var whox = new List<ExtendedWho>();

                // Generate random querytype for WHO query
                int queryType = RandomNumber.Next(0, 999);

                // Add the querytype field if it wasn't defined
                var _fields = fields;
                if ((fields & WhoxField.QueryType) == 0)
                    _fields |= WhoxField.QueryType;

                string whoQuery = string.Format("WHO {0} {1}%{2},{3}", target, flags.AsString(), _fields.AsString(), queryType);
                string queryKey = string.Format("WHO {0} {1} {2:D}", target, queryType, _fields);

                RequestManager.QueueOperation(queryKey, new RequestOperation(whox, ro =>
                {
                    callback?.Invoke((ExtendedWho)ro.State);
                }));
                SendRawMessage(whoQuery);
            }
        }

        /// <summary>
        /// Requests the mode of a channel from the server.
        /// </summary>
        public void GetMode(string channel)

M ChatSharp/IrcClient.cs => ChatSharp/IrcClient.cs +12 -0
@@ 43,6 43,8 @@ namespace ChatSharp
            return new DateTime(1970, 1, 1).AddSeconds(time);
        }

        internal static Random RandomNumber { get; private set; }

        private const int ReadBufferLength = 1024;

        private byte[] ReadBuffer { get; set; }


@@ 170,6 172,8 @@ namespace ChatSharp
            });

            IsNegotiatingCapabilities = false;

            RandomNumber = new Random();
        }

        /// <summary>


@@ 571,5 575,13 @@ namespace ChatSharp
        {
            if (UserQuit != null) UserQuit(this, e);
        }
        /// <summary>
        /// Occurs when a WHO (WHOX protocol) is received.
        /// </summary>
        public event EventHandler<WhoxReceivedEventArgs> WhoxReceived;
        internal void OnWhoxReceived(WhoxReceivedEventArgs e)
        {
            WhoxReceived?.Invoke(this, e);
        }
    }
}

M ChatSharp/RequestManager.cs => ChatSharp/RequestManager.cs +1 -1
@@ 11,7 11,7 @@ namespace ChatSharp
            PendingOperations = new Dictionary<string, RequestOperation>();
        }

        private Dictionary<string, RequestOperation> PendingOperations { get; set; }
        internal Dictionary<string, RequestOperation> PendingOperations { get; private set; }

        public void QueueOperation(string key, RequestOperation operation)
        {

M ChatSharp/ServerInfo.cs => ChatSharp/ServerInfo.cs +5 -0
@@ 14,6 14,7 @@ namespace ChatSharp
            Prefixes = new[] { "ovhaq", "@+%&~" };
            SupportedChannelModes = new ChannelModes();
            IsGuess = true;
            ExtendedWho = false;
        }

        /// <summary>


@@ 116,6 117,10 @@ namespace ChatSharp
        /// Set to the maximum length of an away message
        /// </summary>
        public int? MaxAwayLength { get; set; }
        /// <summary>
        /// Server supports WHOX (WHO extension)
        /// </summary>
        public bool ExtendedWho { get; set; }

        /// <summary>
        /// Modes a server supports that are applicable to channels.

A ChatSharp/WhoX.cs => ChatSharp/WhoX.cs +267 -0
@@ 0,0 1,267 @@
using System;
using System.Collections.Generic;
using System.Net;
namespace ChatSharp
{
    /// <summary>
    /// The results of an IRC WHO (WHOX protocol) query. Depending on what information you request,
    /// some of these fields may be null.
    /// </summary>
    public class ExtendedWho
    {
        internal ExtendedWho()
        {
            QueryType = -1;
            Channel = "*";
            User = new IrcUser();
            IP = string.Empty;
            Server = string.Empty;
            Flags = string.Empty;
            Hops = -1;
            TimeIdle = -1;
            Account = "*";
            OpLevel = "n/a";
        }

        /// <summary>
        /// Type of the query. Defaults to a randomly generated number so ChatSharp can keep
        /// track of WHOX queries it issues.
        /// </summary>
        public int QueryType { get; internal set; }
        /// <summary>
        /// Channel name
        /// </summary>
        public string Channel { get; internal set; }
        /// <summary>
        /// User
        /// </summary>
        public IrcUser User { get; internal set; }
        /// <summary>
        /// Numeric IP address of the user (unresolved hostname)
        /// </summary>
        public string IP { get; internal set; }
        /// <summary>
        /// Server name
        /// </summary>
        public string Server { get; internal set; }
        /// <summary>
        /// User flags
        /// </summary>
        public string Flags { get; internal set; }
        /// <summary>
        /// Distance, in hops
        /// </summary>
        public int Hops { get; internal set; }
        /// <summary>
        /// Time the user has been idle for
        /// </summary>
        public int TimeIdle { get; internal set; }
        /// <summary>
        /// User account name (NickServ or similar)
        /// </summary>
        public string Account { get; internal set; }
        /// <summary>
        /// OP level of the user in the channel
        /// </summary>
        public string OpLevel { get; internal set; }
    }

    /// <summary>
    /// Field matching flags for WHOX protocol.
    /// </summary>
    [Flags]
    public enum WhoxFlag
    {
        /// <summary>
        /// Do not match any flag at all. By doing so, ircds defaults to 'nuhsr'
        /// (everything except the numeric IP).
        /// </summary>
        None = 0,
        /// <summary>
        /// Matches nick (in nick!user@host)
        /// </summary>
        Nick = 1,
        /// <summary>
        /// Matches username (in nick!user@host)
        /// </summary>
        Username = 2,
        /// <summary>
        /// Matches hostname (in nick!user@host)
        /// </summary>
        Hostname = 4,
        /// <summary>
        /// Matches numeric IPs
        /// </summary>
        NumericIp = 8,
        /// <summary>
        /// Matches server name
        /// </summary>
        ServerName = 16,
        /// <summary>
        /// Matches informational text
        /// </summary>
        Info = 32,
        /// <summary>
        /// Matches account name
        /// </summary>
        AccountName = 64,
        /// <summary>
        /// Matches visible and invisble users in a channel
        /// </summary>
        DelayedChanMembers = 128,
        /// <summary>
        /// Matches IRC operators
        /// </summary>
        IrcOp = 256,
        /// <summary>
        /// Special purpose flag, normally only IRC ops have access to it.
        /// </summary>
        Special = 512,
        /// <summary>
        /// Matches all of the flags defined.
        /// </summary>
        All = ~0
    }

    /// <summary>
    /// Information fields for WHOX protocol.
    /// </summary>
    [Flags]
    public enum WhoxField
    {
        /// <summary>
        /// Do not include any field at all.
        /// By doing so, ircds defaults to sending a normal WHO reply.
        /// </summary>
        None = 0,
        /// <summary>
        /// Includes the querytype in the reply
        /// </summary>
        QueryType = 1,
        /// <summary>
        /// Includes the first channel name
        /// </summary>
        Channel = 2,
        /// <summary>
        /// Includes the userID (username)
        /// </summary>
        Username = 4,
        /// <summary>
        /// Includes the IP
        /// </summary>
        UserIp = 8,
        /// <summary>
        /// Includes the user's hostname
        /// </summary>
        Hostname = 16,
        /// <summary>
        /// Includes the server name
        /// </summary>
        ServerName = 32,
        /// <summary>
        /// Includes the user's nick
        /// </summary>
        Nick = 64,
        /// <summary>
        /// Includes all flags a user has
        /// </summary>
        Flags = 128,
        /// <summary>
        /// Includes the "distance" in hops
        /// </summary>
        Hops = 256,
        /// <summary>
        /// Includes the idle time (0 for remote users)
        /// </summary>
        TimeIdle = 512,
        /// <summary>
        /// Includes the user's account name
        /// </summary>
        AccountName = 1024,
        /// <summary>
        /// Includes the user's op level in the channel
        /// </summary>
        OpLevel = 2048,
        /// <summary>
        /// Includes the user's real name
        /// </summary>
        RealName = 4096,
        /// <summary>
        /// Includes all fields defined
        /// </summary>
        All = ~0
    }

    internal static class WhoxEnumExtensions
    {
        public static string AsString(this WhoxFlag flag)
        {
            // nuhisradox
            var result = string.Empty;
            if ((flag & WhoxFlag.Nick) != 0)
                result += 'n';
            if ((flag & WhoxFlag.Username) != 0)
                result += 'u';
            if ((flag & WhoxFlag.Hostname) != 0)
                result += 'h';
            if ((flag & WhoxFlag.NumericIp) != 0)
                result += 'i';
            if ((flag & WhoxFlag.ServerName) != 0)
                result += 's';
            if ((flag & WhoxFlag.Info) != 0)
                result += 'r';
            if ((flag & WhoxFlag.AccountName) != 0)
                result += 'a';
            if ((flag & WhoxFlag.DelayedChanMembers) != 0)
                result += 'd';
            if ((flag & WhoxFlag.IrcOp) != 0)
                result += 'o';
            if ((flag & WhoxFlag.Special) != 0)
                result += 'x';

            if (flag == WhoxFlag.None)
                result = string.Empty;

            return result;
        }

        public static string AsString(this WhoxField field)
        {
            // cdfhilnrstuao
            var result = string.Empty;

            if ((field & WhoxField.Channel) != 0)
                result += 'c';
            if ((field & WhoxField.Hops) != 0)
                result += 'd';
            if ((field & WhoxField.Flags) != 0)
                result += 'f';
            if ((field & WhoxField.Hostname) != 0)
                result += 'h';
            if ((field & WhoxField.UserIp) != 0)
                result += 'i';
            if ((field & WhoxField.TimeIdle) != 0)
                result += 'l';
            if ((field & WhoxField.Nick) != 0)
                result += 'n';
            if ((field & WhoxField.RealName) != 0)
                result += 'r';
            if ((field & WhoxField.ServerName) != 0)
                result += 's';
            if ((field & WhoxField.QueryType) != 0)
                result += 't';
            if ((field & WhoxField.Username) != 0)
                result += 'u';
            if ((field & WhoxField.AccountName) != 0)
                result += 'a';
            if ((field & WhoxField.OpLevel) != 0)
                result += 'o';

            if (field == WhoxField.None)
                result = string.Empty;

            return result;

        }
    }
}