From 1bf8afc68b42c22173478358b26a52ed5a7b22cd Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 25 Mar 2024 05:06:51 -0500 Subject: [PATCH] Added ability to construct IRC messages from parts when sending them. --- TwitchIrcClient/IRC/IrcConnection.cs | 48 +++++++++++++++++-- TwitchIrcClient/IRC/IrcMessageType.cs | 19 +++++++- .../IRC/Messages/ReceivedMessage.cs | 22 ++++----- TwitchIrcClient/Program.cs | 10 ++-- 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/TwitchIrcClient/IRC/IrcConnection.cs b/TwitchIrcClient/IRC/IrcConnection.cs index 2f66ce8..ea8252a 100644 --- a/TwitchIrcClient/IRC/IrcConnection.cs +++ b/TwitchIrcClient/IRC/IrcConnection.cs @@ -139,12 +139,52 @@ namespace TwitchIrcClient.IRC var bytes = Encoding.UTF8.GetBytes(line + ENDL); _Stream.Write(bytes, 0, bytes.Length); } + //TODO make this unit testable? + /// + /// Construct an IRC message from parts and sends it. Does little to no validation on inputs. + /// + /// + /// + /// + /// + public void SendMessage(IrcMessageType command, IEnumerable? parameters = null, + Dictionary? tags = null, string? prefix = null) + { + var message = ""; + if (tags is not null && tags.Count != 0) + { + message = "@" + string.Join(';', + tags.OrderBy(p => p.Key).Select(p => $"{p.Key}={EscapeTagValue(p.Value)}")) + + " "; + } + if (prefix is not null && !string.IsNullOrWhiteSpace(prefix)) + message += ":" + prefix + " "; + message += command.ToCommand() + " "; + if (parameters is not null && parameters.Any()) + { + message += string.Join(' ', parameters.SkipLast(1)); + message += " :" + parameters.Last(); + } + SendLine(message); + } + private static string EscapeTagValue(string? s) + { + if (s is null) + return ""; + return string.Join("", s.Select(c => c switch + { + ';' => @"\:", + ' ' => @"\s", + '\\' => @"\\", + '\r' => @"\r", + '\n' => @"\n", + char ch => ch.ToString(), + })); + } public void Authenticate(string? user, string? pass) { - if (user == null) - user = $"justinfan{Random.Shared.NextInt64(10000):D4}"; - if (pass == null) - pass = "pass"; + user ??= $"justinfan{Random.Shared.NextInt64(10000):D4}"; + pass ??= "pass"; SendLine($"NICK {user}"); SendLine($"PASS {pass}"); } diff --git a/TwitchIrcClient/IRC/IrcMessageType.cs b/TwitchIrcClient/IRC/IrcMessageType.cs index 9eefa07..33743d6 100644 --- a/TwitchIrcClient/IRC/IrcMessageType.cs +++ b/TwitchIrcClient/IRC/IrcMessageType.cs @@ -6,6 +6,9 @@ using System.Threading.Tasks; namespace TwitchIrcClient.IRC { + /// + /// Represents the "command" of an IRC message. + /// public enum IrcMessageType { //twitch standard messages @@ -174,12 +177,26 @@ namespace TwitchIrcClient.IRC } public static class IrcMessageTypeHelper { - //parses a string that is either a numeric code or the command name + /// + /// Parses a string that is either a numeric code or the command name. + /// + /// + /// + /// + /// The value range 000-999 is reserved for numeric commands, and will + /// be converted to a numeric string when forming a message. + /// public static IrcMessageType Parse(string s) { if (int.TryParse(s, out int result)) return (IrcMessageType)result; return Enum.Parse(s); } + public static string ToCommand(this IrcMessageType type) + { + if ((int)type >= 0 && (int)type < 1000) + return $"{(int)type,3}"; + return type.ToString(); + } } } diff --git a/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs b/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs index 958ff35..24ded53 100644 --- a/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs +++ b/TwitchIrcClient/IRC/Messages/ReceivedMessage.cs @@ -96,19 +96,19 @@ namespace TwitchIrcClient.IRC.Messages } return message.MessageType switch { - IrcMessageType.CLEARCHAT => new ClearChat(message), - IrcMessageType.CLEARMSG => new ClearMsg(message), - IrcMessageType.JOIN => new Join(message), + IrcMessageType.CLEARCHAT => new ClearChat(message), + IrcMessageType.CLEARMSG => new ClearMsg(message), + IrcMessageType.JOIN => new Join(message), IrcMessageType.GLOBALUSERSTATE => new GlobalUserState(message), - IrcMessageType.HOSTTARGET => new HostTarget(message), - IrcMessageType.NOTICE => new Notice(message), - IrcMessageType.PART => new Part(message), - IrcMessageType.PRIVMSG => new Privmsg(message), - IrcMessageType.ROOMSTATE => new Roomstate(message), + IrcMessageType.HOSTTARGET => new HostTarget(message), + IrcMessageType.NOTICE => new Notice(message), + IrcMessageType.PART => new Part(message), + IrcMessageType.PRIVMSG => new Privmsg(message), + IrcMessageType.ROOMSTATE => new Roomstate(message), IrcMessageType.RPL_NAMREPLY => new NamReply(message), - IrcMessageType.USERNOTICE => new UserNotice(message), - IrcMessageType.USERSTATE => new UserState(message), - IrcMessageType.WHISPER => new Whisper(message), + IrcMessageType.USERNOTICE => new UserNotice(message), + IrcMessageType.USERSTATE => new UserState(message), + IrcMessageType.WHISPER => new Whisper(message), _ => message, }; } diff --git a/TwitchIrcClient/Program.cs b/TwitchIrcClient/Program.cs index e4ffc2f..d0c341a 100644 --- a/TwitchIrcClient/Program.cs +++ b/TwitchIrcClient/Program.cs @@ -9,11 +9,9 @@ RateLimiter limiter = new(20, 30); bool ssl = true; async Task CreateConnection(string channel) { - IrcConnection connection; - if (ssl) - connection = new IrcConnection("irc.chat.twitch.tv", 6697, limiter, true, true); - else - connection = new IrcConnection("irc.chat.twitch.tv", 6667, limiter, true, false); + IrcConnection connection = ssl + ? connection = new IrcConnection("irc.chat.twitch.tv", 6697, limiter, true, true) + : connection = new IrcConnection("irc.chat.twitch.tv", 6667, limiter, true, false); connection.AddCallback(new MessageCallbackItem( (o, m) => { @@ -51,7 +49,7 @@ async Task CreateConnection(string channel) } Console.Write("Channel: "); var channelName = Console.ReadLine(); -ArgumentNullException.ThrowIfNull(channelName, nameof(Channel)); +ArgumentNullException.ThrowIfNullOrWhiteSpace(channelName, nameof(channelName)); var connection = await CreateConnection(channelName); while (true) {