From 8d93fef2e72622580999058b558c9cec9b3b0460 Mon Sep 17 00:00:00 2001 From: AoshiW Date: Thu, 7 Aug 2025 22:13:30 +0200 Subject: [PATCH] update command identifier to `string` --- README.md | 2 +- TwitchLib.Client.Example/Program.cs | 2 +- TwitchLib.Client.Models/CommandInfo.cs | 35 +++++++-------- TwitchLib.Client.Test/CommandInfoTest.cs | 28 +++++++++--- TwitchLib.Client/Interfaces/ITwitchClient.cs | 4 +- TwitchLib.Client/TwitchClient.cs | 46 ++++++++++++-------- 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c3b75df4..8d924345 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Version 4.0.1 contains breaking changes. - Removed obsolete methods. - Methods are now asynchronous. (The return value changed from `void` to `Task` and gains `Async` suffix) - Events are now asynchronous (return value changed from `void` to `Task`) -- `Add/RemoveChatCommandIdentifier` methods were removed, use `ChatCommandIdentifiers` property instead (same applies to whisper); +- `Add/RemoveChatCommandIdentifier` methods were removed, use `ChatCommandIdentifiers` property instead (same applies to whisper) and uses `string` instead of `char` - `OnLog` event was removed (you can still use `ILoggerFactory` to get logs) - removed builders classes (removed `TwitchLib.Client.Models.Builders namespace`) - changed public fields to properties diff --git a/TwitchLib.Client.Example/Program.cs b/TwitchLib.Client.Example/Program.cs index 435aa816..3ad01537 100644 --- a/TwitchLib.Client.Example/Program.cs +++ b/TwitchLib.Client.Example/Program.cs @@ -11,7 +11,7 @@ var credentials = new ConnectionCredentials(); // anonymous user, add Username and OAuth token to get the ability to send messages var client = new TwitchClient(loggerFactory: loggerFactory) { - ChatCommandIdentifiers = { '!', '?' }, // you can customize the command identifiers, if not set, defaults to '!' + ChatCommandIdentifiers = { "!", "?" }, // you can customize the command identifiers, if not set, defaults to '!' }; client.Initialize(credentials); diff --git a/TwitchLib.Client.Models/CommandInfo.cs b/TwitchLib.Client.Models/CommandInfo.cs index d3f55a58..889664f6 100644 --- a/TwitchLib.Client.Models/CommandInfo.cs +++ b/TwitchLib.Client.Models/CommandInfo.cs @@ -6,7 +6,7 @@ namespace TwitchLib.Client.Models; public class CommandInfo { /// Property representing the command identifier (ie command prefix). - public char Identifier { get; } + public string Identifier { get; } /// Property representing the actual command (without the command prefix). public string Name { get; } @@ -20,13 +20,13 @@ public class CommandInfo /// /// Initializes a new instance of the class. /// - public CommandInfo(char identifier, string name) : this(identifier, name, string.Empty, new()) + public CommandInfo(string identifier, string name) : this(identifier, name, string.Empty, new()) { } /// /// Initializes a new instance of the class. /// - public CommandInfo(char identifier, string name, string argumentsAsString, List argumentsAsList) + public CommandInfo(string identifier, string name, string argumentsAsString, List argumentsAsList) { Identifier = identifier; Name = name; @@ -35,37 +35,34 @@ public CommandInfo(char identifier, string name, string argumentsAsString, List< } /// - /// Tries to parse a span of characters into a value. + /// Tries to parse a message with specified command identifier into a value. /// - /// The span of characters to parse. - /// When this method returns, contains the result of successfully parsing s, or an undefined value on failure. /// true if s was successfully parsed; otherwise, false. #if NETSTANDARD2_0 - public static bool TryParse(ReadOnlySpan s, out CommandInfo result) + internal static bool TryParse(string commandIdentifier, ReadOnlySpan message, out CommandInfo result) #else - public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out CommandInfo result) + internal static bool TryParse(string commandIdentifier, ReadOnlySpan message, [MaybeNullWhen(false)] out CommandInfo result) #endif { result = default!; - s = s.Trim(); - if (s.IsEmpty) + if(!message.StartsWith(commandIdentifier.AsSpan())) return false; - var commandIdentifier = s[0]; - s = s.Slice(1); - if (s.IsEmpty || s[0] == ' ') // if string contains only the identifier or the first char after identifier is space, then it is invalid input + + message = message.Slice(commandIdentifier.Length); + if (message.IsEmpty || message[0] == ' ') // if string contains only the identifier or the first char after identifier is space, then it is invalid input return false; - var indexOfSpace = s.IndexOf(' '); + var indexOfSpace = message.IndexOf(' '); if (indexOfSpace == -1) { - var name = s.ToString(); + var name = message.ToString(); result = new(commandIdentifier, name); } else { - var name = s.Slice(0, indexOfSpace).ToString(); - s = s.Slice(indexOfSpace + 1).TrimStart(); - var argumentsAsString = s.ToString(); - result = new(commandIdentifier, name, argumentsAsString, ParseArgumentsToList(s)); + var name = message.Slice(0, indexOfSpace).ToString(); + message = message.Slice(indexOfSpace + 1).TrimStart(); + var argumentsAsString = message.ToString(); + result = new(commandIdentifier, name, argumentsAsString, ParseArgumentsToList(message)); } return true; diff --git a/TwitchLib.Client.Test/CommandInfoTest.cs b/TwitchLib.Client.Test/CommandInfoTest.cs index c8fc7353..d7571f62 100644 --- a/TwitchLib.Client.Test/CommandInfoTest.cs +++ b/TwitchLib.Client.Test/CommandInfoTest.cs @@ -6,12 +6,30 @@ namespace TwitchLib.Client.Test; public class CommandInfoTest { [Theory] - [InlineData("")] - [InlineData("!")] - [InlineData("! command")] - public void ParsingFailAndReturnNull(string s) + [InlineData("", "")] + [InlineData("!", "!")] + [InlineData("!", "! command")] + [InlineData("?", "!command")] + public void ParsingFailAndReturnNull(string commandIdentifier, string message) { - Assert.False(CommandInfo.TryParse(s, out var commandInfo)); + Assert.False(CommandInfo.TryParse(commandIdentifier, message, out var commandInfo)); Assert.Null(commandInfo); } + + [Theory] + [InlineData("!", "!command", 0)] + [InlineData("!", "!command arg1", 1)] + [InlineData("!", "!command arg1 arg2", 2)] + [InlineData("!", "!command arg1 arg2 arg3 arg4", 4)] + [InlineData("!", "!command \"arg1 with space\"", 1)] + [InlineData("!", "!command \"arg1 with space\" \"arg2 with space\"", 2)] + [InlineData("cmd!", "cmd!command", 0)] + [InlineData("cmd!", "cmd!command arg1", 1)] + [InlineData("cmd!", "cmd!command \"arg1 with space\" \"arg2 with space\"", 2)] + public void Parsing(string commandIdentifier, string message, int argCount) + { + Assert.True(CommandInfo.TryParse(commandIdentifier, message, out var commandInfo)); + Assert.NotNull(commandInfo); + Assert.Equal(argCount, commandInfo.ArgumentsAsList.Count); + } } diff --git a/TwitchLib.Client/Interfaces/ITwitchClient.cs b/TwitchLib.Client/Interfaces/ITwitchClient.cs index 67b049cb..b9ee3192 100644 --- a/TwitchLib.Client/Interfaces/ITwitchClient.cs +++ b/TwitchLib.Client/Interfaces/ITwitchClient.cs @@ -57,11 +57,11 @@ public interface ITwitchClient /// /// The chat command identifiers /// - ICollection ChatCommandIdentifiers { get; } + ICollection ChatCommandIdentifiers { get; } /// /// The whisper command identifiers /// - ICollection WhisperCommandIdentifiers { get; } + ICollection WhisperCommandIdentifiers { get; } /// /// Fires when an Announcement is received diff --git a/TwitchLib.Client/TwitchClient.cs b/TwitchLib.Client/TwitchClient.cs index 993fcacf..b8f3cc25 100644 --- a/TwitchLib.Client/TwitchClient.cs +++ b/TwitchLib.Client/TwitchClient.cs @@ -89,10 +89,10 @@ public class TwitchClient : ITwitchClient #region Public Variables /// - public ICollection ChatCommandIdentifiers { get; } = new HashSet(); + public ICollection ChatCommandIdentifiers { get; } = new HashSet(); /// - public ICollection WhisperCommandIdentifiers { get; } = new HashSet(); + public ICollection WhisperCommandIdentifiers { get; } = new HashSet(); /// #if NET @@ -370,9 +370,9 @@ private void InitializationHelper( ConnectionCredentials = credentials; if (ChatCommandIdentifiers.Count == 0) - ChatCommandIdentifiers.Add('!'); + ChatCommandIdentifiers.Add("!"); if (WhisperCommandIdentifiers.Count == 0) - WhisperCommandIdentifiers.Add('!'); + WhisperCommandIdentifiers.Add("!"); for (var i = 0; i < channels.Count; i++) { @@ -898,13 +898,10 @@ private async Task HandlePrivMsg(IrcMessage ircMessage) await OnUserIntro.Invoke(this, new(chatMessage)); } - if (OnChatCommandReceived is not null - && !string.IsNullOrEmpty(chatMessage.Message) - && ChatCommandIdentifiers.Contains(chatMessage.Message[0]) - && CommandInfo.TryParse(chatMessage.Message.AsSpan(), out var commandInfo) - ) + if (OnChatCommandReceived is not null + && CanInvokeCommand(ChatCommandIdentifiers, chatMessage.Message.AsSpan(), out var commandInfo)) { - await OnChatCommandReceived.Invoke(this, new(chatMessage, commandInfo)); + await OnChatCommandReceived.Invoke(this, new(chatMessage, commandInfo!)); } } @@ -1101,16 +1098,29 @@ private async Task HandleWhisper(IrcMessage ircMessage) await OnWhisperReceived.TryInvoke(this, new(whisperMessage)); - if (OnWhisperCommandReceived is not null - && !string.IsNullOrEmpty(whisperMessage.Message) - && WhisperCommandIdentifiers.Contains(whisperMessage.Message[0]) - && CommandInfo.TryParse(whisperMessage.Message.AsSpan(), out var commandInfo) - ) + if (OnWhisperCommandReceived is not null + && CanInvokeCommand(WhisperCommandIdentifiers, whisperMessage.Message.AsSpan(), out var commandInfo)) { - await OnWhisperCommandReceived.Invoke(this, new(whisperMessage, commandInfo)); + await OnWhisperCommandReceived.Invoke(this, new(whisperMessage, commandInfo!)); } } + static bool CanInvokeCommand(ICollection commandIdentifiers, ReadOnlySpan message, out CommandInfo? commandInfo) + { + commandInfo = null; + if (message.IsEmpty) + return false; + + foreach (var commandIdentifier in commandIdentifiers) + { + if (message.StartsWith(commandIdentifier.AsSpan())) + { + return CommandInfo.TryParse(commandIdentifier, message, out commandInfo); + } + } + return false; + } + /// /// Handles the state of the room. /// @@ -1169,9 +1179,9 @@ private static Task HandleCap(IrcMessage _) return Task.CompletedTask; } - #endregion +#endregion - #endregion +#endregion private Task UnaccountedFor(string ircString) {