add more robust parsing tests

This commit is contained in:
Ben Harris 2024-04-19 14:52:01 -04:00
parent 5ca368ea22
commit 32d3731be9
25 changed files with 1119 additions and 358 deletions

View File

@ -9,7 +9,7 @@ namespace ChatSharp.Events
{
internal IrcNoticeEventArgs(IrcMessage message)
{
if (message.Parameters.Length != 2)
if (message.Parameters.Count != 2)
throw new IrcProtocolException("NOTICE was delivered in incorrect format");
Message = message;
}
@ -29,6 +29,6 @@ namespace ChatSharp.Events
/// The source of the notice (often a user).
/// </summary>
/// <value>The source.</value>
public string Source => Message.Prefix;
public string Source => Message.Source;
}
}

View File

@ -59,7 +59,7 @@ namespace ChatSharp.Handlers
// Check if the enabled capabilities count is the same as the ones
// acknowledged by the server.
if (client.IsNegotiatingCapabilities &&
client.Capabilities.Enabled.Count() == acceptedCaps.Count() && !client.IsAuthenticatingSasl)
client.Capabilities.Enabled.Count() == acceptedCaps.Length && !client.IsAuthenticatingSasl)
{
client.SendRawMessage("CAP END");
client.IsNegotiatingCapabilities = false;
@ -76,7 +76,7 @@ namespace ChatSharp.Handlers
// Check if the disabled capabilities count is the same as the ones
// rejected by the server.
if (client.IsNegotiatingCapabilities &&
client.Capabilities.Disabled.Count() == rejectedCaps.Count())
client.Capabilities.Disabled.Count() == rejectedCaps.Length)
{
client.SendRawMessage("CAP END");
client.IsNegotiatingCapabilities = false;
@ -103,7 +103,7 @@ namespace ChatSharp.Handlers
wantCaps.AddRange(newCaps.Where(cap =>
client.Capabilities.Contains(cap) && !client.Capabilities[cap].IsEnabled));
client.SendRawMessage(string.Format("CAP REQ :{0}", string.Join(" ", wantCaps)));
client.SendRawMessage($"CAP REQ :{string.Join(" ", wantCaps)}");
break;
case "DEL":
var disabledCaps = message.Parameters[2].Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)

View File

@ -10,7 +10,7 @@ namespace ChatSharp.Handlers
{
public static void HandleJoin(IrcClient client, IrcMessage message)
{
var user = client.Users.GetOrAdd(message.Prefix);
var user = client.Users.GetOrAdd(message.Source);
var channel = client.Channels.GetOrAdd(message.Parameters[0]);
if (channel != null)
@ -51,7 +51,7 @@ namespace ChatSharp.Handlers
if (!client.Channels.Contains(message.Parameters[0]))
return; // we aren't in this channel, ignore
var user = client.Users.Get(message.Prefix);
var user = client.Users.Get(message.Source);
var channel = client.Channels[message.Parameters[0]];
if (user.Channels.Contains(channel))
@ -92,10 +92,7 @@ namespace ChatSharp.Handlers
if (!user.Channels.Contains(channel))
user.Channels.Add(channel);
if (!user.ChannelModes.ContainsKey(channel))
user.ChannelModes.Add(channel, modes);
else
user.ChannelModes[channel] = modes;
user.ChannelModes[channel] = modes;
}
}
else
@ -117,10 +114,7 @@ namespace ChatSharp.Handlers
if (!user.Channels.Contains(channel))
user.Channels.Add(channel);
if (!user.ChannelModes.ContainsKey(channel))
user.ChannelModes.Add(channel, modes);
else
user.ChannelModes[channel] = modes;
user.ChannelModes[channel] = modes;
}
}
}
@ -165,7 +159,7 @@ namespace ChatSharp.Handlers
var kicked = channel.Users[message.Parameters[1]];
if (kicked.Channels.Contains(channel))
kicked.Channels.Remove(channel);
client.OnUserKicked(new KickEventArgs(channel, new IrcUser(message.Prefix),
client.OnUserKicked(new KickEventArgs(channel, new IrcUser(message.Source),
kicked, message.Parameters[2]));
}
}

View File

@ -4,7 +4,7 @@ namespace ChatSharp.Handlers
{
public static void HandleBanListPart(IrcClient client, IrcMessage message)
{
var parameterString = message.RawMessage[message.RawMessage.IndexOf(' ')..];
var parameterString = message.Format()[message.Format().IndexOf(' ')..];
var parameters = parameterString[parameterString.IndexOf(' ')..].Split(' ');
var request = client.RequestManager.PeekOperation("GETMODE b " + parameters[1]);
var list = (MaskCollection)request.State;
@ -20,7 +20,7 @@ namespace ChatSharp.Handlers
public static void HandleExceptionListPart(IrcClient client, IrcMessage message)
{
var parameterString = message.RawMessage[(message.RawMessage.IndexOf(' ') + 1)..];
var parameterString = message.Format()[(message.Format().IndexOf(' ') + 1)..];
var parameters = parameterString[(parameterString.IndexOf(' ') + 1)..].Split(' ');
var request = client.RequestManager.PeekOperation("GETMODE e " + parameters[1]);
var list = (MaskCollection)request.State;
@ -36,7 +36,7 @@ namespace ChatSharp.Handlers
public static void HandleInviteListPart(IrcClient client, IrcMessage message)
{
var parameterString = message.RawMessage[(message.RawMessage.IndexOf(' ') + 1)..];
var parameterString = message.Format()[(message.Format().IndexOf(' ') + 1)..];
var parameters = parameterString[(parameterString.IndexOf(' ') + 1)..].Split(' ');
var request = client.RequestManager.PeekOperation("GETMODE I " + parameters[1]);
var list = (MaskCollection)request.State;
@ -52,7 +52,7 @@ namespace ChatSharp.Handlers
public static void HandleQuietListPart(IrcClient client, IrcMessage message)
{
var parameterString = message.RawMessage[(message.RawMessage.IndexOf(' ') + 1)..];
var parameterString = message.Format()[(message.Format().IndexOf(' ') + 1)..];
var parameters = parameterString[(parameterString.IndexOf(' ') + 1)..].Split(' ');
var request = client.RequestManager.PeekOperation("GETMODE q " + parameters[1]);
var list = (MaskCollection)request.State;

View File

@ -14,7 +14,7 @@ namespace ChatSharp.Handlers
public static void HandleMOTD(IrcClient client, IrcMessage message)
{
if (message.Parameters.Length != 2)
if (message.Parameters.Count != 2)
throw new IrcProtocolException("372 MOTD message is incorrectly formatted.");
var part = message.Parameters[1][2..];
MOTD += part + Environment.NewLine;

View File

@ -92,7 +92,7 @@ namespace ChatSharp.Handlers
public static void HandleNick(IrcClient client, IrcMessage message)
{
var user = client.Users.Get(message.Prefix);
var user = client.Users.Get(message.Source);
var oldNick = user.Nick;
user.Nick = message.Parameters[0];
@ -106,7 +106,7 @@ namespace ChatSharp.Handlers
public static void HandleQuit(IrcClient client, IrcMessage message)
{
var user = new IrcUser(message.Prefix);
var user = new IrcUser(message.Source);
if (client.User.Nick != user.Nick)
{
client.Users.Remove(user);
@ -182,17 +182,16 @@ namespace ChatSharp.Handlers
continue;
}
if (channel.Mode == null)
channel.Mode = string.Empty;
channel.Mode ??= string.Empty;
// TODO: Support the ones here that aren't done properly
if (client.ServerInfo.SupportedChannelModes.ParameterizedSettings.Contains(c))
{
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Prefix),
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Source),
(add ? "+" : "-") + c + " " + message.Parameters[i++]));
}
else if (client.ServerInfo.SupportedChannelModes.ChannelLists.Contains(c))
{
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Prefix),
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Source),
(add ? "+" : "-") + c + " " + message.Parameters[i++]));
}
else if (client.ServerInfo.SupportedChannelModes.ChannelUserModes.Contains(c))
@ -218,7 +217,7 @@ namespace ChatSharp.Handlers
user.ChannelModes[channel] = null;
}
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Prefix),
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Source),
(add ? "+" : "-") + c + " " + message.Parameters[i++]));
}
@ -234,7 +233,7 @@ namespace ChatSharp.Handlers
channel.Mode = channel.Mode.Replace(c.ToString(), string.Empty);
}
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Prefix),
client.OnModeChanged(new ModeChangeEventArgs(channel.Name, new IrcUser(message.Source),
(add ? "+" : "-") + c));
}
}

View File

@ -19,13 +19,12 @@ namespace ChatSharp.Handlers
{
var chunk = b64Bytes.Take(400).ToArray();
b64Bytes = b64Bytes.Skip(400).ToArray();
client.SendRawMessage(string.Format("AUTHENTICATE {0}", Encoding.UTF8.GetString(chunk)));
client.SendRawMessage($"AUTHENTICATE {Encoding.UTF8.GetString(chunk)}");
}
if (b64Bytes.Length > 0)
client.SendRawMessage(string.Format("AUTHENTICATE {0}", Encoding.UTF8.GetString(b64Bytes)));
else
client.SendRawMessage("AUTHENTICATE +");
client.SendRawMessage(b64Bytes.Length > 0
? $"AUTHENTICATE {Encoding.UTF8.GetString(b64Bytes)}"
: "AUTHENTICATE +");
client.IsAuthenticatingSasl = false;
}

View File

@ -6,8 +6,7 @@ namespace ChatSharp.Handlers
{
public static void HandleISupport(IrcClient client, IrcMessage message)
{
if (client.ServerInfo == null)
client.ServerInfo = new ServerInfo();
client.ServerInfo ??= new ServerInfo();
foreach (var item in message.Parameters)
{
string key, value;
@ -86,12 +85,11 @@ namespace ChatSharp.Handlers
break;
}
else
switch (key.ToUpper())
client.ServerInfo.ExtendedWho = key.ToUpper() switch
{
case "WHOX":
client.ServerInfo.ExtendedWho = true;
break;
}
"WHOX" => true,
_ => client.ServerInfo.ExtendedWho
};
}
client.OnServerInfoReceived(new SupportsEventArgs(client.ServerInfo));
@ -101,9 +99,8 @@ namespace ChatSharp.Handlers
{
// 004 sendak.freenode.net ircd-seven-1.1.3 DOQRSZaghilopswz CFILMPQbcefgijklmnopqrstvz bkloveqjfI
// TODO: Figure out how to properly handle this message
if (client.ServerInfo == null)
client.ServerInfo = new ServerInfo();
if (message.Parameters.Length >= 5)
client.ServerInfo ??= new ServerInfo();
if (message.Parameters.Count >= 5)
foreach (var c in message.Parameters[4])
if (!client.ServerInfo.SupportedChannelModes.ChannelUserModes.Contains(c))
client.ServerInfo.SupportedChannelModes.ChannelUserModes += c.ToString();

View File

@ -9,7 +9,7 @@ namespace ChatSharp.Handlers
{
public static void HandleWhoIsUser(IrcClient client, IrcMessage message)
{
if (message.Parameters != null && message.Parameters.Length >= 6)
if (message.Parameters != null && message.Parameters.Count >= 6)
{
var whois = (WhoIs)client.RequestManager.PeekOperation("WHOIS " + message.Parameters[1]).State;
whois.User.Nick = message.Parameters[1];
@ -212,7 +212,7 @@ namespace ChatSharp.Handlers
whox.User.RealName = message.Parameters[fieldIdx];
fieldIdx++;
}
} while (fieldIdx < message.Parameters.Length - 1);
} while (fieldIdx < message.Parameters.Count - 1);
whoxList.Add(whox);
}
@ -255,13 +255,13 @@ namespace ChatSharp.Handlers
public static void HandleAccount(IrcClient client, IrcMessage message)
{
var user = client.Users.GetOrAdd(message.Prefix);
var user = client.Users.GetOrAdd(message.Source);
user.Account = message.Parameters[0];
}
public static void HandleChangeHost(IrcClient client, IrcMessage message)
{
var user = client.Users.Get(message.Prefix);
var user = client.Users.Get(message.Source);
// Only handle CHGHOST for users we know
if (user != null)

View File

@ -85,9 +85,9 @@ namespace ChatSharp
if (Channels.Contains(channel))
throw new InvalidOperationException("Client is already present in channel.");
var joinCmd = string.Format("JOIN {0}", channel);
var joinCmd = $"JOIN {channel}";
if (!string.IsNullOrEmpty(key))
joinCmd += string.Format(" {0}", key);
joinCmd += $" {key}";
SendRawMessage(joinCmd, channel);
@ -187,7 +187,7 @@ namespace ChatSharp
var whoQuery = string.Format("WHO {0} {1}%{2},{3}", target, flags.AsString(), _fields.AsString(),
queryType);
var queryKey = string.Format("WHO {0} {1} {2:D}", target, queryType, _fields);
var queryKey = $"WHO {target} {queryType} {_fields:D}";
RequestManager.QueueOperation(queryKey,
new RequestOperation(whox, ro => { callback?.Invoke((List<ExtendedWho>)ro.State); }));
@ -197,7 +197,7 @@ namespace ChatSharp
{
var whox = new List<ExtendedWho>();
var whoQuery = string.Format("WHO {0}", target);
var whoQuery = $"WHO {target}";
RequestManager.QueueOperation(whoQuery,
new RequestOperation(whox, ro => { callback?.Invoke((List<ExtendedWho>)ro.State); }));

View File

@ -91,10 +91,7 @@ namespace ChatSharp
if (parts.Length > 2 || parts.Length == 0)
throw new FormatException("Server address is not in correct format ('hostname:port')");
ServerHostname = parts[0];
if (parts.Length > 1)
ServerPort = int.Parse(parts[1]);
else
ServerPort = 6667;
ServerPort = parts.Length > 1 ? int.Parse(parts[1]) : 6667;
}
}
@ -197,7 +194,7 @@ namespace ChatSharp
/// </summary>
public void ConnectAsync()
{
if (Socket != null && Socket.Connected)
if (Socket is { Connected: true })
throw new InvalidOperationException("Socket is already connected to server.");
Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ReadBuffer = new byte[ReadBufferLength];
@ -259,10 +256,9 @@ namespace ChatSharp
NetworkStream = new NetworkStream(Socket);
if (UseSSL)
{
if (IgnoreInvalidSSL)
NetworkStream = new SslStream(NetworkStream, false, (a, b, c, d) => true);
else
NetworkStream = new SslStream(NetworkStream);
NetworkStream = IgnoreInvalidSSL
? new SslStream(NetworkStream, false, (a, b, c, d) => true)
: new SslStream(NetworkStream);
((SslStream)NetworkStream).AuthenticateAsClient(ServerHostname);
}
@ -334,9 +330,9 @@ namespace ChatSharp
{
OnRawMessageReceived(new RawMessageEventArgs(rawMessage, false));
var message = new IrcMessage(rawMessage);
if (Handlers.ContainsKey(message.Command.ToUpper()))
if (Handlers.TryGetValue(message.Command, out var handler))
{
Handlers[message.Command.ToUpper()](this, message);
handler(this, message);
}
}
@ -370,7 +366,7 @@ namespace ChatSharp
/// </summary>
public void SendIrcMessage(IrcMessage message)
{
SendRawMessage(message.RawMessage);
SendRawMessage(message.Format());
}
private void MessageSent(IAsyncResult result)

View File

@ -1,130 +1,243 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace ChatSharp
{
/// <summary>
/// Represents a raw IRC message. This is a low-level construct - PrivateMessage is used
/// to represent messages sent from users.
/// </summary>
public class IrcMessage
public class IrcMessage : IEquatable<IrcMessage>
{
/// <summary>
/// Initializes and decodes an IRC message, given the raw message from the server.
/// </summary>
public IrcMessage(string rawMessage)
private static readonly string[] TagUnescaped = {"\\", " ", ";", "\r", "\n"};
private static readonly string[] TagEscaped = {@"\\", "\\s", "\\:", "\\r", "\\n"};
public IrcMessage()
{
RawMessage = rawMessage;
Tags = Array.Empty<KeyValuePair<string, string>>();
}
public IrcMessage(string command, params string[] parameters)
{
Command = command.ToUpperInvariant();
Parameters = parameters.ToList();
}
/// <summary>
/// Parse and tokenize an IRC message, given the raw message from the server.
/// </summary>
public IrcMessage(string line)
{
if (string.IsNullOrWhiteSpace(line)) throw new ArgumentNullException(nameof(line));
if (rawMessage.StartsWith("@"))
string[] split;
if (line.StartsWith('@'))
{
var rawTags = rawMessage[1..rawMessage.IndexOf(' ')];
rawMessage = rawMessage[(rawMessage.IndexOf(' ') + 1)..];
Tags = new Dictionary<string, string>();
// Parse tags as key value pairs
var tags = new List<KeyValuePair<string, string>>();
foreach (var rawTag in rawTags.Split(';'))
{
var replacedTag = rawTag.Replace(@"\:", ";");
// The spec declares `@a=` as a tag with an empty value, while `@b;` as a tag with a null value
KeyValuePair<string, string> tag = new KeyValuePair<string, string>(replacedTag, null);
split = line.Split(" ", 2);
var messageTags = split[0];
line = split[1];
if (replacedTag.Contains("="))
foreach (var part in messageTags[1..].Split(';'))
if (part.Contains('=', StringComparison.Ordinal))
{
var key = replacedTag.Substring(0, replacedTag.IndexOf("=", StringComparison.Ordinal));
var value = replacedTag[(replacedTag.IndexOf("=", StringComparison.Ordinal) + 1)..];
tag = new KeyValuePair<string, string>(key, value);
split = part.Split('=', 2);
Tags[split[0]] = UnescapeTag(split[1]);
}
else
{
Tags[part] = null;
}
tags.Add(tag);
}
Tags = tags.ToArray();
}
if (rawMessage.StartsWith(":"))
string trailing;
if (line.Contains(" :", StringComparison.Ordinal))
{
Prefix = rawMessage[1..rawMessage.IndexOf(' ')];
rawMessage = rawMessage[(rawMessage.IndexOf(' ') + 1)..];
}
if (rawMessage.Contains(' '))
{
Command = rawMessage.Remove(rawMessage.IndexOf(' '));
rawMessage = rawMessage[(rawMessage.IndexOf(' ') + 1)..];
// Parse parameters
var parameters = new List<string>();
while (!string.IsNullOrEmpty(rawMessage))
{
if (rawMessage.StartsWith(":"))
{
parameters.Add(rawMessage[1..]);
break;
}
if (!rawMessage.Contains(' '))
{
parameters.Add(rawMessage);
rawMessage = string.Empty;
break;
}
parameters.Add(rawMessage.Remove(rawMessage.IndexOf(' ')));
rawMessage = rawMessage[(rawMessage.IndexOf(' ') + 1)..];
}
Parameters = parameters.ToArray();
split = line.Split(" :", 2);
line = split[0];
trailing = split[1];
}
else
{
// Violates RFC 1459, but we'll parse it anyway
Command = rawMessage;
Parameters = Array.Empty<string>();
trailing = null;
}
Parameters = line.Contains(' ', StringComparison.Ordinal)
? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
: new List<string> {line};
if (Parameters[0].StartsWith(':'))
{
Source = Parameters[0][1..];
Parameters.RemoveAt(0);
}
if (Parameters.Count > 0)
{
Command = Parameters[0].ToUpper(CultureInfo.InvariantCulture);
Parameters.RemoveAt(0);
}
if (trailing != null) Parameters.Add(trailing);
// Parse server-time message tag.
// Fallback to server-info if both znc.in/server-info and the former exists.
//
// znc.in/server-time tag
if (Tags.Any(tag => tag.Key == "t"))
if (Tags?.Any(tag => tag.Key == "t") ?? false)
{
var tag = Tags.SingleOrDefault(x => x.Key == "t");
Timestamp = new Timestamp(tag.Value, true);
}
// server-time tag
else if (Tags.Any(tag => tag.Key == "time"))
else if (Tags?.Any(tag => tag.Key == "time") ?? false)
{
var tag = Tags.SingleOrDefault(x => x.Key == "time");
Timestamp = new Timestamp(tag.Value);
}
}
public bool Equals(IrcMessage other)
{
if (other == null) return false;
return Format() == other.Format();
}
public override int GetHashCode()
{
return Format().GetHashCode(StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return Equals(obj as IrcMessage);
}
/// <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>
/// The unparsed message.
/// Escape strings for use in ircv3 tags
/// </summary>
public string RawMessage { get; }
/// <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;
}
/// <summary>
/// Formats self <see cref="IrcMessage" /> 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 (Parameters != null && Parameters.Any())
{
var last = Parameters[^1];
var withoutLast = Parameters.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);
}
/// <summary>
/// The message prefix.
/// The message source.
/// </summary>
public string Prefix { get; }
public string Source { get; set; }
/// <summary>
/// The message command.
/// </summary>
public string Command { get; }
public string Command { get; set; }
/// <summary>
/// Additional parameters supplied with the message.
/// </summary>
public string[] Parameters { get; }
public List<string> Parameters { get; set; }
/// <summary>
/// The message tags.
/// </summary>
public KeyValuePair<string, string>[] Tags { get; }
public Dictionary<string, string> Tags { get; set; }
/// <summary>
/// The message timestamp in ISO 8601 format.

View File

@ -8,6 +8,7 @@ namespace ChatSharp
/// </summary>
public class IrcUser : IEquatable<IrcUser>
{
private readonly string _source;
internal IrcUser()
{
Channels = new ChannelCollection();
@ -18,18 +19,29 @@ namespace ChatSharp
/// <summary>
/// Constructs an IrcUser given a hostmask or nick.
/// </summary>
public IrcUser(string host) : this()
public IrcUser(string source) : this()
{
if (!host.Contains("@") && !host.Contains("!"))
if (source == null) return;
_source = source;
if (source.Contains('@', StringComparison.Ordinal))
{
Nick = host;
var split = source.Split('@');
Nick = split[0];
Hostname = split[1];
}
else
{
var mask = host.Split('@', '!');
Nick = mask[0];
User = mask[1];
Hostname = mask.Length <= 2 ? "" : mask[2];
Nick = source;
}
if (Nick.Contains('!', StringComparison.Ordinal))
{
var userSplit = Nick.Split('!');
Nick = userSplit[0];
User = userSplit[1];
}
}
@ -44,22 +56,6 @@ namespace ChatSharp
Mode = string.Empty;
}
/// <summary>
/// Constructs an IRC user given a nick, user, and password.
/// </summary>
public IrcUser(string nick, string user, string password) : this(nick, user)
{
Password = password;
}
/// <summary>
/// Constructs an IRC user given a nick, user, password, and real name.
/// </summary>
public IrcUser(string nick, string user, string password, string realName) : this(nick, user, password)
{
RealName = realName;
}
/// <summary>
/// The user's nick.
/// </summary>
@ -110,14 +106,15 @@ namespace ChatSharp
/// <summary>
/// This user's hostmask (nick!user@host).
/// </summary>
public string Hostmask => Nick + "!" + User + "@" + Hostname;
public string Hostmask => $"{Nick}!{User}@{Hostname}";
/// <summary>
/// True if this user is equal to another (compares hostmasks).
/// </summary>
public bool Equals(IrcUser other)
{
return other.Hostmask == Hostmask;
if (other == null) return false;
return other._source == _source;
}
/// <summary>
@ -143,8 +140,7 @@ namespace ChatSharp
/// </summary>
public static bool Match(string mask, string value)
{
if (value == null)
value = string.Empty;
value ??= string.Empty;
var i = 0;
var j = 0;
for (; j < value.Length && i < mask.Length; j++)
@ -179,9 +175,7 @@ namespace ChatSharp
/// </summary>
public override bool Equals(object obj)
{
if (obj is IrcUser user)
return Equals(user);
return false;
return Equals(obj as IrcUser);
}
/// <summary>
@ -189,7 +183,7 @@ namespace ChatSharp
/// </summary>
public override int GetHashCode()
{
return Hostmask.GetHashCode();
return _source.GetHashCode(StringComparison.Ordinal);
}
/// <summary>
@ -197,7 +191,7 @@ namespace ChatSharp
/// </summary>
public override string ToString()
{
return Hostmask;
return _source;
}
}
}

View File

@ -12,7 +12,7 @@ namespace ChatSharp
Source = message.Parameters[0];
Message = message.Parameters[1];
User = client.Users.GetOrAdd(message.Prefix);
User = client.Users.GetOrAdd(message.Source);
if (serverInfo.ChannelTypes.Any(c => Source.StartsWith(c.ToString())))
IsChannelMessage = true;
else

View File

@ -115,8 +115,8 @@ namespace ChatSharp
public List<char?> GetModesForNick(string nick)
{
var supportedPrefixes = Prefixes[1];
List<char?> modeList = new List<char?>();
List<char> nickPrefixes = new List<char>();
var modeList = new List<char?>();
var nickPrefixes = new List<char>();
foreach (var prefix in supportedPrefixes)
if (nick.Contains(prefix))

View File

@ -2,7 +2,20 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
<Using Include="System"/>
<Using Include="System.Collections.Generic"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatSharp\ChatSharp.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" Version="15.1.2"/>
</ItemGroup>
<ItemGroup>
<None Update="Data\msg-*.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using YamlDotNet.Serialization;
namespace ChatSharp.Tests.Data;
public class JoinModel
{
public List<Test> Tests { get; set; }
public class Test
{
[YamlMember(Alias = "desc")] public string Description { get; set; }
public Atoms Atoms { get; set; }
public List<string> Matches { get; set; }
}
public class Atoms
{
public Dictionary<string, string> Tags { get; set; }
public string Source { get; set; }
public string Verb { get; set; }
public List<string> Params { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace ChatSharp.Tests.Data;
public class SplitModel
{
public List<Test> Tests { get; set; }
public class Test
{
public string Input { get; set; }
public JoinModel.Atoms Atoms { get; set; }
}
}

View File

@ -0,0 +1,221 @@
# IRC parser tests
# joining atoms into sendable messages
# Written in 2015 by Daniel Oaks <daniel@danieloaks.net>
#
# To the extent possible under law, the author(s) have dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication along
# with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed
# https://github.com/grawity/code/tree/master/lib/tests
# some of the tests here originate from Mozilla's test vectors, which is public domain
# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js
# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here
# https://github.com/SaberUK/ircparser/tree/master/test
tests:
# the desc string holds a description of the test, if it exists
# the atoms dict has the keys:
# * tags: tags dict
# tags with no value are an empty string
# * source: source string, without single leading colon
# * verb: verb string
# * params: params split up as a list
# if the params key does not exist, assume it is empty
# if any other keys do no exist, assume they are null
# a key that is null does not exist or is not specified with the
# given input string
# matches is a list of messages that match
# simple tests
- desc: Simple test with verb and params.
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- "asdf"
matches:
- "foo bar baz asdf"
- "foo bar baz :asdf"
# with no regular params
- desc: Simple test with source and no params.
atoms:
source: "src"
verb: "AWAY"
matches:
- ":src AWAY"
- desc: Simple test with source and empty trailing param.
atoms:
source: "src"
verb: "AWAY"
params:
- ""
matches:
- ":src AWAY :"
# with source
- desc: Simple test with source.
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- "asdf"
matches:
- ":coolguy foo bar baz asdf"
- ":coolguy foo bar baz :asdf"
# with trailing param
- desc: Simple test with trailing param.
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- "asdf quux"
matches:
- "foo bar baz :asdf quux"
- desc: Simple test with empty trailing param.
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- ""
matches:
- "foo bar baz :"
- desc: Simple test with trailing param containing colon.
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- ":asdf"
matches:
- "foo bar baz ::asdf"
# with source and trailing param
- desc: Test with source and trailing param.
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- "asdf quux"
matches:
- ":coolguy foo bar baz :asdf quux"
- desc: Test with trailing containing beginning+end whitespace.
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- " asdf quux "
matches:
- ":coolguy foo bar baz : asdf quux "
- desc: Test with trailing containing what looks like another trailing param.
atoms:
source: "coolguy"
verb: "PRIVMSG"
params:
- "bar"
- "lol :) "
matches:
- ":coolguy PRIVMSG bar :lol :) "
- desc: Simple test with source and empty trailing.
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- ""
matches:
- ":coolguy foo bar baz :"
- desc: Trailing contains only spaces.
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- " "
matches:
- ":coolguy foo bar baz : "
- desc: Param containing tab (tab is not considered SPACE for message splitting).
atoms:
source: "coolguy"
verb: "foo"
params:
- "b\tar"
- "baz"
matches:
- ":coolguy foo b\tar baz"
- ":coolguy foo b\tar :baz"
# with tags
- desc: Tag with no value and space-filled trailing.
atoms:
tags:
"asd": ""
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- " "
matches:
- "@asd :coolguy foo bar baz : "
- desc: Tags with escaped values.
atoms:
verb: "foo"
tags:
"a": "b\\and\nk"
"d": "gh;764"
matches:
- "@a=b\\\\and\\nk;d=gh\\:764 foo"
- "@d=gh\\:764;a=b\\\\and\\nk foo"
- desc: Tags with escaped values and params.
atoms:
verb: "foo"
tags:
"a": "b\\and\nk"
"d": "gh;764"
params:
- "par1"
- "par2"
matches:
- "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2"
- "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2"
- "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2"
- "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"
- desc: Tag with long, strange values (including LF and newline).
atoms:
tags:
foo: "\\\\;\\s \r\n"
verb: "COMMAND"
matches:
- "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"

View File

@ -0,0 +1,343 @@
# IRC parser tests
# splitting messages into usable atoms
# Written in 2015 by Daniel Oaks <daniel@danieloaks.net>
#
# To the extent possible under law, the author(s) have dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication along
# with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed
# https://github.com/grawity/code/tree/master/lib/tests
# some of the tests here originate from Mozilla's test vectors, which is public domain
# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js
# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here
# https://github.com/SaberUK/ircparser/tree/master/test
# we follow RFC1459 with regards to multiple ascii spaces splitting atoms:
# The prefix, command, and all parameters are
# separated by one (or more) ASCII space character(s) (0x20).
# because doing it as RFC2812 says (strictly as a single ascii space) isn't sane
tests:
# input is the string coming directly from the server to parse
# the atoms dict has the keys:
# * tags: tags dict
# tags with no value are an empty string
# * source: source string, without single leading colon
# * verb: verb string
# * params: params split up as a list
# if the params key does not exist, assume it is empty
# if any other keys do no exist, assume they are null
# a key that is null does not exist or is not specified with the
# given input string
# simple
- input: "foo bar baz asdf"
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- "asdf"
# with source
- input: ":coolguy foo bar baz asdf"
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- "asdf"
# with trailing param
- input: "foo bar baz :asdf quux"
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- "asdf quux"
- input: "foo bar baz :"
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- ""
- input: "foo bar baz ::asdf"
atoms:
verb: "foo"
params:
- "bar"
- "baz"
- ":asdf"
# with source and trailing param
- input: ":coolguy foo bar baz :asdf quux"
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- "asdf quux"
- input: ":coolguy foo bar baz : asdf quux "
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- " asdf quux "
- input: ":coolguy PRIVMSG bar :lol :) "
atoms:
source: "coolguy"
verb: "PRIVMSG"
params:
- "bar"
- "lol :) "
- input: ":coolguy foo bar baz :"
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- ""
- input: ":coolguy foo bar baz : "
atoms:
source: "coolguy"
verb: "foo"
params:
- "bar"
- "baz"
- " "
# with tags
- input: "@a=b;c=32;k;rt=ql7 foo"
atoms:
verb: "foo"
tags:
"a": "b"
"c": "32"
"k":
"rt": "ql7"
# with escaped tags
- input: "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
atoms:
verb: "foo"
tags:
"a": "b\\and\nk"
"c": "72 45"
"d": "gh;764"
# with tags and source
- input: "@c;h=;a=b :quux ab cd"
atoms:
tags:
"c":
"h": ""
"a": "b"
source: "quux"
verb: "ab"
params:
- "cd"
# different forms of last param
- input: ":src JOIN #chan"
atoms:
source: "src"
verb: "JOIN"
params:
- "#chan"
- input: ":src JOIN :#chan"
atoms:
source: "src"
verb: "JOIN"
params:
- "#chan"
# with and without last param
- input: ":src AWAY"
atoms:
source: "src"
verb: "AWAY"
- input: ":src AWAY "
atoms:
source: "src"
verb: "AWAY"
# tab is not considered <SPACE>
- input: ":cool\tguy foo bar baz"
atoms:
source: "cool\tguy"
verb: "foo"
params:
- "bar"
- "baz"
# with weird control codes in the source
- input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz"
atoms:
source: "coolguy!ag@net\x035w\x03ork.admin"
verb: "PRIVMSG"
params:
- "foo"
- "bar baz"
- input: ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz"
atoms:
source: "coolguy!~ag@n\x02et\x0305w\x0fork.admin"
verb: "PRIVMSG"
params:
- "foo"
- "bar baz"
- input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3"
atoms:
tags:
tag1: "value1"
tag2:
vendor1/tag3: "value2"
vendor2/tag4: ""
source: "irc.example.com"
verb: "COMMAND"
params:
- "param1"
- "param2"
- "param3 param3"
- input: ":irc.example.com COMMAND param1 param2 :param3 param3"
atoms:
source: "irc.example.com"
verb: "COMMAND"
params:
- "param1"
- "param2"
- "param3 param3"
- input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3"
atoms:
tags:
tag1: "value1"
tag2:
vendor1/tag3: "value2"
vendor2/tag4:
verb: "COMMAND"
params:
- "param1"
- "param2"
- "param3 param3"
- input: "COMMAND"
atoms:
verb: "COMMAND"
# yaml encoding + slashes is fun
- input: "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
atoms:
tags:
foo: "\\\\;\\s \r\n"
verb: "COMMAND"
# broken messages from unreal
- input: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters"
atoms:
source: "gravel.mozilla.org"
verb: "432"
params:
- "#momo"
- "Erroneous Nickname: Illegal characters"
- input: ":gravel.mozilla.org MODE #tckk +n "
atoms:
source: "gravel.mozilla.org"
verb: "MODE"
params:
- "#tckk"
- "+n"
- input: ":services.esper.net MODE #foo-bar +o foobar "
atoms:
source: "services.esper.net"
verb: "MODE"
params:
- "#foo-bar"
- "+o"
- "foobar"
# tag values should be parsed char-at-a-time to prevent wayward replacements.
- input: "@tag1=value\\\\ntest COMMAND"
atoms:
tags:
tag1: "value\\ntest"
verb: "COMMAND"
# If a tag value has a slash followed by a character which doesn't need
# to be escaped, the slash should be dropped.
- input: "@tag1=value\\1 COMMAND"
atoms:
tags:
tag1: "value1"
verb: "COMMAND"
# A slash at the end of a tag value should be dropped
- input: "@tag1=value1\\ COMMAND"
atoms:
tags:
tag1: "value1"
verb: "COMMAND"
# Duplicate tags: Parsers SHOULD disregard all but the final occurence
- input: "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND"
atoms:
tags:
tag1: "5"
tag2: "3"
tag3: "4"
verb: "COMMAND"
# vendored tags can have the same name as a non-vendored tag
- input: "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND"
atoms:
tags:
tag1: "5"
tag2: "3"
tag3: "4"
vendor/tag2: "8"
verb: "COMMAND"
# Some parsers handle /MODE in a special way, make sure they do it right
- input: ":SomeOp MODE #channel :+i"
atoms:
source: "SomeOp"
verb: "MODE"
params:
- "#channel"
- "+i"
- input: ":SomeOp MODE #channel +oo SomeUser :AnotherUser"
atoms:
source: "SomeOp"
verb: "MODE"
params:
- "#channel"
- "+oo"
- "SomeUser"
- "AnotherUser"

View File

@ -1,143 +1,138 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ChatSharp.Tests;
namespace ChatSharp.Tests
[TestClass]
public class IrcMessageTests
{
[TestClass]
public class IrcMessageTests
[TestMethod]
public void NewValidMessage()
{
[TestMethod]
public void NewValidMessage()
try
{
try
{
_ = new IrcMessage(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
}
catch (Exception e)
{
Assert.Fail("Expected no exception, got: {0}", e.Message);
}
_ = new IrcMessage(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
}
[TestMethod]
public void NewValidMessage_Command()
catch (Exception e)
{
IrcMessage fromMessage = new(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
Assert.AreEqual(fromMessage.Command, "PRIVMSG");
}
[TestMethod]
public void NewValidMessage_Prefix()
{
IrcMessage fromMessage = new(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
Assert.AreEqual(fromMessage.Prefix, "user!~ident@host");
}
[TestMethod]
public void NewValidMessage_Params()
{
IrcMessage fromMessage = new(@":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareParams = new[] { "target", "Lorem ipsum dolor sit amet" };
CollectionAssert.AreEqual(fromMessage.Parameters, compareParams);
}
[TestMethod]
public void NewValidMessage_Tags()
{
IrcMessage fromMessage =
new("@a=123;b=456;c=789 :user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", "123"),
new KeyValuePair<string, string>("b", "456"),
new KeyValuePair<string, string>("c", "789")
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void NewValidMessage_Tags02()
{
IrcMessage fromMessage = new("@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello");
var compareTags = new[]
{
new KeyValuePair<string, string>("aaa", "bbb"),
new KeyValuePair<string, string>("ccc", null),
new KeyValuePair<string, string>("example.com/ddd", "eee")
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void NewValidMessage_TagsWithSemicolon()
{
IrcMessage fromMessage =
new(@"@a=123\:456;b=456\:789;c=789\:123 :user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", "123;456"),
new KeyValuePair<string, string>("b", "456;789"),
new KeyValuePair<string, string>("c", "789;123")
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void NewValidMessage_TagsNoValue()
{
IrcMessage fromMessage = new("@a=;b :nick!ident@host.com PRIVMSG me :Hello");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", ""),
new KeyValuePair<string, string>("b", null)
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void Timestamp_CompareISOString()
{
IrcMessage[] messages =
{
new("@time=2011-10-19T16:40:51.620Z :Angel!angel@example.org PRIVMSG Wiz :Hello"),
new("@time=2012-06-30T23:59:59.419Z :John!~john@1.2.3.4 JOIN #chan")
};
string[] timestamps =
{
"2011-10-19T16:40:51.620Z",
"2012-06-30T23:59:59.419Z"
};
Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]);
Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]);
}
[TestMethod]
public void Timestamp_FromTimestamp()
{
IrcMessage[] messages =
[
new("@t=1504923966 :Angel!angel@example.org PRIVMSG Wiz :Hello"),
new("@t=1504923972 :John!~john@1.2.3.4 JOIN #chan")
];
string[] timestamps =
[
"2017-09-09T02:26:06.000Z",
"2017-09-09T02:26:12.000Z"
];
Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]);
Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]);
}
[TestMethod]
public void Timestamp_FailOnLeap()
{
Assert.ThrowsException<ArgumentException>(() =>
new IrcMessage("@time=2012-06-30T23:59:60.419Z :John!~john@1.2.3.4 JOIN #chan"));
Assert.Fail("Expected no exception, got: {0}", e.Message);
}
}
[TestMethod]
public void NewValidMessage_Command()
{
IrcMessage fromMessage = new(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
Assert.AreEqual("PRIVMSG", fromMessage.Command);
}
[TestMethod]
public void NewValidMessage_Prefix()
{
IrcMessage fromMessage = new(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
Assert.AreEqual("user!~ident@host", fromMessage.Source);
}
[TestMethod]
public void NewValidMessage_Params()
{
IrcMessage fromMessage = new(":user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareParams = new[] { "target", "Lorem ipsum dolor sit amet" };
CollectionAssert.AreEqual(compareParams, fromMessage.Parameters);
}
[TestMethod]
public void NewValidMessage_Tags()
{
IrcMessage fromMessage =
new("@a=123;b=456;c=789 :user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", "123"),
new KeyValuePair<string, string>("b", "456"),
new KeyValuePair<string, string>("c", "789")
};
CollectionAssert.AreEqual(compareTags, fromMessage.Tags);
}
[TestMethod]
public void NewValidMessage_Tags02()
{
IrcMessage fromMessage = new("@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello");
var compareTags = new[]
{
new KeyValuePair<string, string>("aaa", "bbb"),
new KeyValuePair<string, string>("ccc", null),
new KeyValuePair<string, string>("example.com/ddd", "eee")
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void NewValidMessage_TagsWithSemicolon()
{
IrcMessage fromMessage =
new(@"@a=123\:456;b=456\:789;c=789\:123 :user!~ident@host PRIVMSG target :Lorem ipsum dolor sit amet");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", "123;456"),
new KeyValuePair<string, string>("b", "456;789"),
new KeyValuePair<string, string>("c", "789;123")
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void NewValidMessage_TagsNoValue()
{
IrcMessage fromMessage = new("@a=;b :nick!ident@host.com PRIVMSG me :Hello");
var compareTags = new[]
{
new KeyValuePair<string, string>("a", ""),
new KeyValuePair<string, string>("b", null)
};
CollectionAssert.AreEqual(fromMessage.Tags, compareTags);
}
[TestMethod]
public void Timestamp_CompareISOString()
{
IrcMessage[] messages =
[
new("@time=2011-10-19T16:40:51.620Z :Angel!angel@example.org PRIVMSG Wiz :Hello"),
new("@time=2012-06-30T23:59:59.419Z :John!~john@1.2.3.4 JOIN #chan")
];
string[] timestamps =
[
"2011-10-19T16:40:51.620Z",
"2012-06-30T23:59:59.419Z"
];
Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]);
Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]);
}
[TestMethod]
public void Timestamp_FromTimestamp()
{
IrcMessage[] messages =
[
new("@t=1504923966 :Angel!angel@example.org PRIVMSG Wiz :Hello"),
new("@t=1504923972 :John!~john@1.2.3.4 JOIN #chan")
];
string[] timestamps =
[
"2017-09-09T02:26:06.000Z",
"2017-09-09T02:26:12.000Z"
];
Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]);
Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]);
}
[TestMethod]
public void Timestamp_FailOnLeap()
{
Assert.ThrowsException<ArgumentException>(() =>
new IrcMessage("@time=2012-06-30T23:59:60.419Z :John!~john@1.2.3.4 JOIN #chan"));
}
}

View File

@ -1,74 +1,71 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ChatSharp.Tests;
namespace ChatSharp.Tests
[TestClass]
public class IrcUserTests
{
[TestClass]
public class IrcUserTests
[TestMethod]
public void GetUserModes_NotNull_FiveModes()
{
[TestMethod]
public void GetUserModes_NotNull_FiveModes()
{
IrcUser user = new("~&@%+aji", "user");
IrcClient client = new("irc.address", user);
IrcUser user = new("~&@%+aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 5);
}
Assert.IsTrue(userModes.Count == 5);
}
[TestMethod]
public void GetUserModes_NotNull_FourModes()
{
IrcUser user = new("&@%+aji", "user");
IrcClient client = new("irc.address", user);
[TestMethod]
public void GetUserModes_NotNull_FourModes()
{
IrcUser user = new("&@%+aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 4);
}
Assert.IsTrue(userModes.Count == 4);
}
[TestMethod]
public void GetUserModes_NotNull_ThreeModes()
{
IrcUser user = new("@%+aji", "user");
IrcClient client = new("irc.address", user);
[TestMethod]
public void GetUserModes_NotNull_ThreeModes()
{
IrcUser user = new("@%+aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 3);
}
Assert.IsTrue(userModes.Count == 3);
}
[TestMethod]
public void GetUserModes_NotNull_TwoModes()
{
IrcUser user = new("%+aji", "user");
IrcClient client = new("irc.address", user);
[TestMethod]
public void GetUserModes_NotNull_TwoModes()
{
IrcUser user = new("%+aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 2);
}
Assert.IsTrue(userModes.Count == 2);
}
[TestMethod]
public void GetUserModes_NotNull_OneMode()
{
IrcUser user = new("+aji", "user");
IrcClient client = new("irc.address", user);
[TestMethod]
public void GetUserModes_NotNull_OneMode()
{
IrcUser user = new("+aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 1);
}
Assert.IsTrue(userModes.Count == 1);
}
[TestMethod]
public void GetUserModes_IsNull()
{
IrcUser user = new("aji", "user");
IrcClient client = new("irc.address", user);
[TestMethod]
public void GetUserModes_IsNull()
{
IrcUser user = new("aji", "user");
IrcClient client = new("irc.address", user);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
var userModes = client.ServerInfo.GetModesForNick(user.Nick);
Assert.IsTrue(userModes.Count == 0);
}
Assert.IsTrue(userModes.Count == 0);
}
}

View File

@ -0,0 +1,57 @@
using System.Globalization;
using System.IO;
using ChatSharp.Tests.Data;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace ChatSharp.Tests;
[TestClass]
public class ParsingTests
{
private static T LoadYaml<T>(string path)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
return deserializer.Deserialize<T>(File.ReadAllText(path));
}
[TestMethod]
public void Split()
{
foreach (var test in LoadYaml<SplitModel>("Data/msg-split.yaml").Tests)
{
var message = new IrcMessage(test.Input);
var atoms = test.Atoms;
Assert.AreEqual(atoms.Verb.ToUpper(CultureInfo.InvariantCulture), message.Command,
$"command failed on: '{test.Input}'");
Assert.AreEqual(atoms.Source, message.Source,
$"source failed on: '{test.Input}' ");
CollectionAssert.AreEqual(atoms.Tags, message.Tags,
$"tags failed on: '{test.Input}' ");
CollectionAssert.AreEqual(atoms.Params ?? [], message.Parameters,
$"params failed on: '{test.Input}' ");
}
}
[TestMethod]
public void Join()
{
foreach (var test in LoadYaml<JoinModel>("Data/msg-join.yaml").Tests)
{
var atoms = test.Atoms;
var line = new IrcMessage
{
Command = atoms.Verb,
Parameters = atoms.Params,
Source = atoms.Source,
Tags = atoms.Tags,
}.Format();
Assert.IsTrue(test.Matches.Contains(line), test.Description);
}
}
}

View File

@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="ChatSharp"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatSharp\ChatSharp.csproj"/>

View File

@ -1,6 +1,4 @@
using ChatSharp;
var client = new IrcClient("irc.libera.chat", new("chatsharp", "chatsharp"));
var client = new IrcClient("irc.libera.chat", new("chatsharp"));
client.ConnectionComplete += (_, _) => client.JoinChannel("##chatsharp");
client.ChannelMessageReceived += (s, e) =>
@ -12,7 +10,7 @@ client.ChannelMessageReceived += (s, e) =>
}
else if (e.PrivateMessage.Message.StartsWith(".ban "))
{
if (!channel.UsersByMode['@'].Contains(client.User))
if (!channel.UsersByMode?['@'].Contains(client.User) ?? false)
{
channel.SendMessage("I'm not an op here!");
return;
@ -21,6 +19,7 @@ client.ChannelMessageReceived += (s, e) =>
var target = e.PrivateMessage.Message[5..];
client.WhoIs(target, whois => channel.ChangeMode($"+b *!*@{whois.User.Hostname}"));
}
Console.WriteLine($"> {e.IrcMessage.Format()}");
};
client.ConnectAsync();