Merge pull request #86 from RockyTV/caps

Support WHOX queries
This commit is contained in:
Drew DeVault 2017-10-04 20:10:55 -04:00 committed by GitHub
commit d1424ba162
9 changed files with 475 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace ChatSharp
@ -145,6 +146,35 @@ namespace ChatSharp
SendRawMessage("WHOIS {0}", nick);
}
/// <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>

View File

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

View File

@ -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)
{

View File

@ -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.

267
ChatSharp/WhoX.cs Normal file
View File

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