diff --git a/MiniTwitch.Irc.Benchmarks/Benchmarks.cs b/MiniTwitch.Irc.Benchmarks/Benchmarks.cs new file mode 100644 index 0000000..ad2d4c9 --- /dev/null +++ b/MiniTwitch.Irc.Benchmarks/Benchmarks.cs @@ -0,0 +1,53 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using MiniTwitch.Irc.Internal.Models; + +namespace MiniTwitch.Irc.Benchmarks; + +[MemoryDiagnoser(false)] +public class Benchmarks +{ + private byte[][] utf8Lines = null!; + private readonly IrcClient _client = new(op => op.Anonymous = true); + + [Params(1, 100, 1000)] + public int LineCount { get; set; } + + [GlobalSetup] + public void AddData() + { + utf8Lines = File.ReadAllText("data.txt").Split('\0').Take(LineCount).Select(Encoding.UTF8.GetBytes).ToArray(); + Console.WriteLine(utf8Lines.Length); + } + + [Benchmark] + public void Create() + { + foreach (var line in utf8Lines.AsSpan()) + { + _ = new IrcMessage(line); + } + } + + [Benchmark] + public void Create_Parse() + { + foreach (var line in utf8Lines.AsSpan()) + { + var msg = new IrcMessage(line); + _ = msg.ParseTags(); + _ = msg.GetChannel(); + _ = msg.GetContent(); + _ = msg.GetUsername(); + } + } + + [Benchmark] + public void Create_Parse_Map_Send() + { + foreach (var line in utf8Lines.AsSpan()) + { + _client.Parse(line); + } + } +} diff --git a/MiniTwitch.Irc.Benchmarks/MiniTwitch.Irc.Benchmarks.csproj b/MiniTwitch.Irc.Benchmarks/MiniTwitch.Irc.Benchmarks.csproj new file mode 100644 index 0000000..aa43ff6 --- /dev/null +++ b/MiniTwitch.Irc.Benchmarks/MiniTwitch.Irc.Benchmarks.csproj @@ -0,0 +1,22 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/MiniTwitch.Irc.Benchmarks/Program.cs b/MiniTwitch.Irc.Benchmarks/Program.cs new file mode 100644 index 0000000..7d49125 --- /dev/null +++ b/MiniTwitch.Irc.Benchmarks/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; +using MiniTwitch.Irc.Benchmarks; + +BenchmarkRunner.Run(); diff --git a/MiniTwitch.Irc.Benchmarks/data.txt b/MiniTwitch.Irc.Benchmarks/data.txt new file mode 100644 index 0000000..01d5d6b Binary files /dev/null and b/MiniTwitch.Irc.Benchmarks/data.txt differ diff --git a/MiniTwitch.Irc.Test/CommandParsingTests.cs b/MiniTwitch.Irc.Test/CommandParsingTests.cs index 79adb48..fbc2501 100644 --- a/MiniTwitch.Irc.Test/CommandParsingTests.cs +++ b/MiniTwitch.Irc.Test/CommandParsingTests.cs @@ -1,5 +1,6 @@ using System.Text; using MiniTwitch.Irc.Internal.Enums; +using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; using Xunit; @@ -10,78 +11,67 @@ public class CommandParsingTests public void Parse_PRIVMSG() { string raw = "@badge-info=subscriber/42;badges=subscriber/42,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=ffc033d5-862c-413c-8422-72a59aa1b0f1;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1678752306574;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #pajlada :btw DFantasy, have you gotten an unused Dragon Hunter Crossbow just laying around by any chance? kkOna"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.PRIVMSG, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.PRIVMSG, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_USERNOTICE() { string raw = "@badge-info=subscriber/15;badges=subscriber/12;color=#BAFF2F;display-name=VOJTERIS;emotes=;flags=;id=267bffe6-534c-4ddc-a11c-d32b242d8734;login=vojteris;mod=0;msg-id=resub;msg-param-cumulative-months=15;msg-param-months=0;msg-param-multimonth-duration=0;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=look\\sat\\sthose\\sshitty\\semotes,\\srip\\s$5\\sLUL;msg-param-sub-plan=1000;msg-param-was-gifted=false;room-id=11148817;subscriber=1;system-msg=VOJTERIS\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s15\\smonths!;tmi-sent-ts=1678743959736;user-id=75436142;user-type= :tmi.twitch.tv USERNOTICE #pajlada :egFors TeaTime"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.USERNOTICE, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.USERNOTICE, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_CLEARCHAT() { string raw = "@ban-duration=30;room-id=11148817;target-user-id=783267696;tmi-sent-ts=1678712873805 :tmi.twitch.tv CLEARCHAT #pajlada :occluder"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.CLEARCHAT, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.CLEARCHAT, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_WHISPER() { string raw = "@badges=;color=#2E8B57;display-name=pajbot;emotes=25:7-11;message-id=7;thread-id=82008718_783267696;turbo=0;user-id=82008718;user-type= :pajbot!pajbot@pajbot.tmi.twitch.tv WHISPER occluder :Riftey Kappa"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.WHISPER, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.WHISPER, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_ROOMSTATE() { string raw = "@emote-only=0;followers-only=-1;r9k=0;room-id=783267696;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #occluder"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.ROOMSTATE, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.ROOMSTATE, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_NOTICE() { string raw = "@msg-id=msg_channel_suspended :tmi.twitch.tv NOTICE #foretack :This channel does not exist or has been suspended."; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.NOTICE, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.NOTICE, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_CLEARMSG() { string raw = "@login=occluder;room-id=;target-msg-id=55dc74c9-a6b2-4443-9b68-3446a5ddb7ed;tmi-sent-ts=1678798254260 :tmi.twitch.tv CLEARMSG #occluder :@emote-only=0;followers-only=-1;r9k=0;room-id=783267696;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #occluder "; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.CLEARMSG, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.CLEARMSG, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_USERSTATE() { string raw = "@badge-info=;badges=broadcaster/1;color=#F2647B;display-name=occluder;emote-sets=0,4236,15961,19194,376284588,385292559,477339272,5fc5e142-d394-481f-a561-5406c2ba3ef0,e21484e8-cf11-48b1-8b67-c180fa39f926;mod=0;subscriber=0;user-type= :tmi.twitch.tv USERSTATE #occluder"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.USERSTATE, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.USERSTATE, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_RECONNECT() { string raw = ":tmi.twitch.tv RECONNECT"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.RECONNECT, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.RECONNECT, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_PING() { string raw = "PING :tmi.twitch.tv"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.PING, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.PING, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_PONG() { string raw = "PONG :tmi.twitch.tv"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - Assert.Equal(IrcCommand.PONG, IrcParsing.ParseCommand(span).cmd); + Assert.Equal(IrcCommand.PONG, new IrcMessage(Encoding.UTF8.GetBytes(raw)).Command); } [Fact] public void Parse_Multiple() diff --git a/MiniTwitch.Irc.Test/IrcParsingTests.cs b/MiniTwitch.Irc.Test/IrcParsingTests.cs index bc4b3be..1b4662d 100644 --- a/MiniTwitch.Irc.Test/IrcParsingTests.cs +++ b/MiniTwitch.Irc.Test/IrcParsingTests.cs @@ -1,4 +1,5 @@ using System.Text; +using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; using Xunit; @@ -9,18 +10,7 @@ public class IrcParsingTests public void Find_Channel() { string raw = ":foo!foo@foo.tmi.twitch.tv JOIN #bar"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string channel = IrcParsing.FindChannel(span); - - Assert.Equal("bar", channel); - } - - [Fact] - public void Find_Channel_AnySeparator() - { - string raw = ":foo!foo@foo.tmi.twitch.tv JOIN #bar\r\n"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string channel = IrcParsing.FindChannel(span, true); + string channel = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetChannel(); Assert.Equal("bar", channel); } @@ -29,8 +19,7 @@ public void Find_Channel_AnySeparator() public void Find_Content() { string raw = "@badge-info=subscriber/11;badges=subscriber/6;color=#F2647B;display-name=occluder;emotes=;first-msg=0;flags=;id=e674e393-1230-4a89-bebc-fae1f925e52c;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1680255594264;turbo=0;user-id=783267696;user-type= :occluder!occluder@occluder.tmi.twitch.tv PRIVMSG #pajlada :Are you on some dank browser jammehcow"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string content = IrcParsing.FindContent(span).Content; + string content = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetContent().Content; Assert.Equal("Are you on some dank browser jammehcow", content); } @@ -39,8 +28,7 @@ public void Find_Content() public void Find_Content_Empty() { string raw = "@badge-info=subscriber/5;badges=subscriber/3;color=#5F9EA0;display-name=Syn993;emotes=;flags=;id=401d17b8-363a-4f63-85c8-cd5996fbd4e0;login=syn993;mod=0;msg-id=resub;msg-param-cumulative-months=5;msg-param-months=0;msg-param-multimonth-duration=0;msg-param-multimonth-tenure=0;msg-param-should-share-streak=1;msg-param-streak-months=4;msg-param-sub-plan-name=Channel\\sSubscription\\s(mandeow);msg-param-sub-plan=1000;msg-param-was-gifted=false;room-id=128856353;subscriber=1;system-msg=Syn993\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s5\\smonths,\\scurrently\\son\\sa\\s4\\smonth\\sstreak!;tmi-sent-ts=1678873100296;user-id=79085174;user-type= :tmi.twitch.tv USERNOTICE #mande"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string content = IrcParsing.FindContent(span, maybeEmpty: true).Content; + string content = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetContent().Content; Assert.Equal(string.Empty, content); } @@ -49,19 +37,16 @@ public void Find_Content_Empty() public void Find_Content_Action() { string raw = "@badge-info=subscriber/1;badges=subscriber/0;color=#9ACD32;display-name=FeelsCzechMan;emotes=425671:13-20;first-msg=0;flags=;id=0012619e-8e14-4d51-93c9-e9d6fd5a178b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1679730451594;turbo=0;user-id=875745889;user-type= :feelsczechman!feelsczechman@feelsczechman.tmi.twitch.tv PRIVMSG #pajlada :\u0001ACTION FeelsDankMan PowerUpR DANK WAVE▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂\u0001"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - (string content, bool action) = IrcParsing.FindContent(span, maybeAction: true); - - Assert.True(action); + (string content, bool action) = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetContent(maybeAction: true); Assert.Equal("FeelsDankMan PowerUpR DANK WAVE▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂", content); + Assert.True(action); } [Fact] public void Find_Content_InvalidAction() { string raw = "@badge-info=subscriber/1;badges=subscriber/0;color=#9ACD32;display-name=FeelsCzechMan;emotes=425671:13-20;first-msg=0;flags=;id=0012619e-8e14-4d51-93c9-e9d6fd5a178b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1679730451594;turbo=0;user-id=875745889;user-type= :feelsczechman!feelsczechman@feelsczechman.tmi.twitch.tv PRIVMSG #pajlada :\u0001ACTION \u0001"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - (string content, bool action) = IrcParsing.FindContent(span, maybeAction: true); + (string content, bool action) = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetContent(); Assert.False(action); Assert.Equal("\u0001ACTION \u0001", content); @@ -71,8 +56,7 @@ public void Find_Content_InvalidAction() public void Find_Username() { string raw = "@badge-info=subscriber/1;badges=subscriber/0;color=#9ACD32;display-name=FeelsCzechMan;emotes=425671:13-20;first-msg=0;flags=;id=0012619e-8e14-4d51-93c9-e9d6fd5a178b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1679730451594;turbo=0;user-id=875745889;user-type= :feelsczechman!feelsczechman@feelsczechman.tmi.twitch.tv PRIVMSG #pajlada :\u0001ACTION FeelsDankMan PowerUpR DANK WAVE▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂▂▃▄▅▆▇██▇▆▅▄▃▂\u0001"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string username = IrcParsing.FindUsername(span); + string username = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetUsername(); Assert.Equal("feelsczechman", username); } @@ -81,8 +65,7 @@ public void Find_Username() public void Find_Username_NoTags() { string raw = ":pajapajapaja!pajapajapaja@pajapajapaja.tmi.twitch.tv JOIN #bar"; - ReadOnlySpan span = Encoding.UTF8.GetBytes(raw).AsSpan(); - string username = IrcParsing.FindUsername(span, true); + string username = new IrcMessage(Encoding.UTF8.GetBytes(raw)).GetUsername(); Assert.Equal("pajapajapaja", username); } diff --git a/MiniTwitch.Irc/AssemblyInfo.cs b/MiniTwitch.Irc/AssemblyInfo.cs index 297c946..476f62d 100644 --- a/MiniTwitch.Irc/AssemblyInfo.cs +++ b/MiniTwitch.Irc/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("MiniTwitch.Irc.Test")] +[assembly: InternalsVisibleTo("MiniTwitch.Irc.Benchmarks")] diff --git a/MiniTwitch.Irc/Internal/Enums/Tags.cs b/MiniTwitch.Irc/Internal/Enums/Tags.cs new file mode 100644 index 0000000..936d370 --- /dev/null +++ b/MiniTwitch.Irc/Internal/Enums/Tags.cs @@ -0,0 +1,66 @@ +namespace MiniTwitch.Irc.Internal.Enums; + +internal enum Tags +{ + Id = 205, + R9K = 278, + Mod = 320, + Vip = 335, + Bits = 434, + Slow = 453, + Flags = 525, + Login = 537, + Color = 543, + Turbo = 556, + MsgId = 577, + Badges = 614, + Emotes = 653, + RoomId = 695, + UserId = 697, + ThreadId = 882, + FirstMsg = 924, + SubsOnly = 940, + UserType = 942, + BadgeInfo = 972, + MessageId = 991, + EmoteSets = 1030, + EmoteOnly = 1033, + SystemMsg = 1049, + Subscriber = 1076, + TmiSentTs = 1093, + ClientNonce = 1215, + BanDuration = 1220, + DisplayName = 1220, + TargetMsgId = 1269, + TargetUserId = 1389, + FollowersOnly = 1484, + MsgParamColor = 1489, + MsgParamMonths = 1611, + MsgParamSubPlan = 1748, + ReturningChatter = 1782, + ReplyParentMsgId = 1873, + ReplyParentUserId = 1993, + MsgParamSenderName = 2049, + MsgParamGiftMonths = 2082, + ReplyParentMsgBody = 2098, + MsgParamViewerCount = 2125, + PinnedChatPaidLevel = 2139, + MsgParamRecipientId = 2159, + MsgParamSenderLogin = 2169, + MsgParamSenderCount = 2185, + MsgParamSubPlanName = 2210, + PinnedChatPaidAmount = 2263, + MsgParamStreakMonths = 2306, + ReplyParentUserLogin = 2325, + MsgParamMassGiftCount = 2451, + PinnedChatPaidCurrency = 2478, + PinnedChatPaidExponent = 2484, + ReplyParentDisplayName = 2516, + ReplyThreadParentMsgId = 2550, + MsgParamCumulativeMonths = 2743, + MsgParamRecipientUserName = 2863, + MsgParamShouldShareStreak = 2872, + ReplyThreadParentUserLogin = 3002, + MsgparamRecipientDisplayName = 3174, + PinnedChatPaidIsSystemMessage = 3331, +} diff --git a/MiniTwitch.Irc/Internal/Models/IrcMessage.cs b/MiniTwitch.Irc/Internal/Models/IrcMessage.cs new file mode 100644 index 0000000..03043bf --- /dev/null +++ b/MiniTwitch.Irc/Internal/Models/IrcMessage.cs @@ -0,0 +1,192 @@ +using System.Runtime.CompilerServices; +using MiniTwitch.Common.Extensions; +using MiniTwitch.Irc.Internal.Enums; +using MiniTwitch.Irc.Internal.Parsing; + +namespace MiniTwitch.Irc.Internal.Models; + +internal readonly ref struct IrcMessage +{ + public readonly bool HasTags { get; init; } = default; + public readonly Range TagsRange { get; init; } = default; + public readonly bool HasUsername { get; init; } = default; + public readonly Range UsernameRange { get; init; } = default; + public readonly IrcCommand Command { get; init; } = default; + public readonly bool IsGlobalChannel { get; init; } = default; + public readonly Range ChannelRange { get; init; } = default; + public readonly bool HasMessageContent { get; init; } = default; + public readonly Range MessageContentRange { get; init; } = default; + public readonly bool IsMultipleMessages { get; init; } = default; + public readonly int NextMessageStartIndex { get; init; } = default; + public readonly ReadOnlyMemory Memory { get; init; } = default; + + public IrcMessage(ReadOnlyMemory memory) + { + const byte colon = (byte)':'; + const byte at = (byte)'@'; + const byte space = (byte)' '; + const byte excl = (byte)'!'; + const byte asterisk = (byte)'*'; + const byte cr = (byte)'\r'; + + ReadOnlySpan span = memory.Span; + this.Memory = memory; + switch (span[0]) + { + case at: + this.HasTags = true; + // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf + // ----->| + this.TagsRange = 1..span.IndexOf(space); + int usernameStart = this.TagsRange.End.Value + 2; + + AfterUsernameStart: + + // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf + // -->| + int usernameEnd = span[usernameStart..].IndexOf(excl) + usernameStart; + if (usernameEnd - usernameStart is not -1 and not > 25) + { + this.HasUsername = true; + this.UsernameRange = usernameStart..usernameEnd; + } + + int commandStartAddVal = this.HasUsername ? usernameEnd : usernameStart; + int commandStart = span[commandStartAddVal..].IndexOf(space) + commandStartAddVal + 1; + int commandEnd = span[commandStart..].IndexOf(space) + commandStart; + if (commandEnd - commandStart == -1) + { + this.Command = (IrcCommand)span[commandStart..span.Length].Sum(); + return; + } + + this.Command = (IrcCommand)span[commandStart..commandEnd].Sum(); + int contentStart; + if (span[commandEnd + 1] == asterisk) + { + this.IsGlobalChannel = true; + contentStart = commandEnd + 4; + } + else + { + int channelStart = commandEnd + 2; + int channelEnd = span[channelStart..].IndexOfAny(space, cr) + channelStart; + if (channelEnd - channelStart == -1) + { + this.ChannelRange = channelStart..span.Length; + return; + } + + this.ChannelRange = channelStart..channelEnd; + if (span[channelEnd] == cr) + { + this.IsMultipleMessages = true; + this.NextMessageStartIndex = channelEnd + 2; + return; + } + + contentStart = channelEnd + 2; + } + + // Didn't end at channel so there must be content + this.HasMessageContent = true; + int contentEnd = span[contentStart..].IndexOf(cr) + contentStart; + if (contentEnd - contentStart == -1) + { + this.MessageContentRange = contentStart..span.Length; + return; + } + + this.MessageContentRange = contentStart..contentEnd; + if (span.Length > contentEnd + 1) + { + this.IsMultipleMessages = true; + this.NextMessageStartIndex = contentEnd + 2; + } + break; + + case colon: + usernameStart = 1; + goto AfterUsernameStart; + + default: + commandStart = 0; + commandEnd = span.IndexOf(space); + this.Command = (IrcCommand)span[commandStart..commandEnd].Sum(); + int crIndex = span[commandEnd..].IndexOf(cr) + commandEnd; + if (crIndex - commandEnd != -1) + { + this.IsMultipleMessages = true; + this.NextMessageStartIndex = crIndex + 2; + } + + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly string GetChannel() => TagHelper.GetString(this.Memory.Span[this.ChannelRange], true); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly string GetUsername() => TagHelper.GetString(this.Memory.Span[this.UsernameRange]); + + public readonly (string Content, bool Action) GetContent(bool maybeAction = false) + { + if (!this.HasMessageContent) + return (string.Empty, false); + + string content = TagHelper.GetString(this.Memory.Span[this.MessageContentRange]); + if (maybeAction && content.Length > 9 && content[0] == '\u0001' && content[^1] == '\u0001') + return (content[8..^1], true); + + return (content, false); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly IrcTags ParseTags() + { + const byte semiColon = (byte)';'; + const byte equals = (byte)'='; + + ReadOnlySpan span = this.Memory.Span; + int tagCount = 0; + + // Determine the amount of tags by counting '=' + int eqIndex; + ReadOnlySpan tagIndexCountSpan = span; + while ((eqIndex = tagIndexCountSpan.IndexOf(equals)) != -1) + { + tagCount++; + tagIndexCountSpan = tagIndexCountSpan[(eqIndex + 1)..]; + } + + IrcTags tags = new(tagCount); + Index tagsEndIndex = this.TagsRange.End; + Index tagStart = this.TagsRange.Start; + Index tagEquals; + Index tagEnd; + + for (int i = 0; i < tagCount; i++) + { + if (tagStart.Value >= tagsEndIndex.Value) + break; + + // Index of '=' from the start of the tag + tagEquals = span[tagStart..tagsEndIndex].IndexOf(equals) + tagStart.Value; + if (tagEquals.Value == tagStart.Value - 1) + break; + + // Index of ';' from the equal sign of the tag + tagEnd = span[tagEquals..tagsEndIndex].IndexOf(semiColon) + tagEquals.Value; + // Account for last tag, which doesn't end with a semicolon + if (tagEnd.Value == tagEquals.Value - 1) + tagEnd = tagsEndIndex; + + // Key memory slice: from index 0 until the next equal sign + // Value memory slice: from index 0 after the equal sign until next semicolon + tags.Add(i, this.Memory[tagStart..tagEquals], this.Memory[(tagEquals.Value + 1)..tagEnd]); + tagStart = tagEnd.Value + 1; + } + + return tags; + } +} diff --git a/MiniTwitch.Irc/Internal/Parsing/IrcParsing.cs b/MiniTwitch.Irc/Internal/Parsing/IrcParsing.cs deleted file mode 100644 index 658ffdf..0000000 --- a/MiniTwitch.Irc/Internal/Parsing/IrcParsing.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System.Text; -using MiniTwitch.Common.Extensions; -using MiniTwitch.Irc.Internal.Enums; -using MiniTwitch.Irc.Internal.Models; - -namespace MiniTwitch.Irc.Internal.Parsing; - -internal static class IrcParsing -{ - internal static string FindChannel(this ReadOnlySpan span, bool anySeparator = false) - { - const byte numberSymbol = (byte)'#'; - const byte space = (byte)' '; - const byte lf = (byte)'\n'; - const byte cr = (byte)'\r'; - - int scopeStart = span.IndexOf(space) + 1; - int symbolIndex = span[scopeStart..].IndexOf(numberSymbol) + scopeStart; - int nextSpace; - if (anySeparator) - { - ReadOnlySpan ends = stackalloc byte[] { space, lf, cr }; - nextSpace = span[symbolIndex..].IndexOfAny(ends) + symbolIndex; - } - else - { - nextSpace = span[symbolIndex..].IndexOf(space) + symbolIndex; - } - - if (nextSpace == symbolIndex - 1) - nextSpace = span.Length; - - ReadOnlySpan newSpan; - try - { - newSpan = span[(symbolIndex + 1)..nextSpace]; - } - catch (Exception) - { - throw new ArgumentException($"Failed to find channel from {symbolIndex + 1} to {nextSpace} in\n{Encoding.UTF8.GetString(span)}"); - } - return TagHelper.GetString(newSpan, true); - } - - internal static (string Content, bool Action) FindContent(this ReadOnlySpan span, bool maybeEmpty = false, bool maybeAction = false) - { - const byte colon = (byte)':'; - const byte space = (byte)' '; - - // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf - // ↑ - int firstSeparatorIndex = span.IndexOf(space) + 2; - // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf - // ↑ - int secondSeparatorIndex = span[firstSeparatorIndex..].IndexOf(colon) + firstSeparatorIndex + 1; - if (maybeEmpty && secondSeparatorIndex - firstSeparatorIndex - 1 == -1) - return (string.Empty, false); - - ReadOnlySpan newSpan = span[secondSeparatorIndex..]; - string content = TagHelper.GetString(newSpan); - if (maybeAction && content.Length > 9 && content[0] == '\u0001' && content[^1] == '\u0001') - return (content[8..^1], true); - - return (content, false); - } - - internal static string FindUsername(this ReadOnlySpan span, bool noTags = false) - { - const byte space = (byte)' '; - const byte exclamationMark = (byte)'!'; - - int separator; - int exclamationIndex; - if (noTags) - { - // :foo!foo@foo.tmi.twitch.tv JOIN #bar - // |__| - separator = 1; - exclamationIndex = span.IndexOf(exclamationMark); - } - else - { - // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf - // ↑ - separator = span.IndexOf(space) + 2; - // :foo!foo@foo.tmi.twitch.tv PRIVMSG #bar :asdf - // ↑ - exclamationIndex = span[separator..].IndexOf(exclamationMark) + separator; - } - - ReadOnlySpan newSpan; - try - { - newSpan = span[separator..exclamationIndex]; - } - catch (Exception) - { - throw new ArgumentException($"Failed to find channel from {separator + 1} to {exclamationIndex} in\n{Encoding.UTF8.GetString(span)}"); - } - return TagHelper.GetString(newSpan); - } - - internal static IrcTags ParseTags(ReadOnlyMemory memory) - { - ReadOnlySpan span = memory.Span; - - const byte at = (byte)'@'; - const byte space = (byte)' '; - const byte colon = (byte)':'; - const byte semiColon = (byte)';'; - const byte equals = (byte)'='; - - int tagsStartIndex = span[0] is at or colon ? 1 : 0; - int tagsEndIndex = span.IndexOf(space); - int tagCount = 0; - - // Determine the amount of tags by counting '=' - int eqIndex; - ReadOnlySpan tagIndexCountSpan = span; - while ((eqIndex = tagIndexCountSpan.IndexOf(equals)) != -1) - { - tagCount++; - tagIndexCountSpan = tagIndexCountSpan[(eqIndex + 1)..]; - } - - IrcTags tags = new(tagCount); - int tagStart = tagsStartIndex; - int tagEquals; - int tagEnd; - - for (int i = 0; i < tagCount; i++) - { - if (tagStart >= tagsEndIndex) - break; - - // Index of '=' from the start of the tag - tagEquals = span[tagStart..tagsEndIndex].IndexOf(equals) + tagStart; - if (tagEquals == tagStart - 1) - break; - - // Index of ';' from the equal sign of the tag - tagEnd = span[tagEquals..tagsEndIndex].IndexOf(semiColon) + tagEquals; - // Account for last tag, which doesn't end with a semicolon - if (tagEnd == tagEquals - 1) - tagEnd = tagsEndIndex; - - // Key memory slice: from index 0 until the next equal sign - // Value memory slice: from index 0 after the equal sign until next semicolon - tags.Add(i, memory[tagStart..tagEquals], memory[(tagEquals + 1)..tagEnd]); - tagStart = tagEnd + 1; - } - - return tags; - } - - /// - /// Parses IRC commands from messages - /// - /// The span to parse - /// The IRC command and the index start of the next message (0 if none) - internal static (IrcCommand cmd, int lfIndex) ParseCommand(ReadOnlySpan span) - { - const byte space = (byte)' '; - const byte colon = (byte)':'; - const byte at = (byte)'@'; - const byte lf = (byte)'\n'; - - int scopeStart; - int firstSpace; - int startIndex; - int length; - ReadOnlySpan command; - - if (span[0] == lf) - { - return (IrcCommand.Unknown, 0); - } - else if (span[0] is not colon and not at) - { - firstSpace = span.IndexOf(space); - command = span[..firstSpace]; - } - else - { - // Start looking for the command from the first space - scopeStart = span.IndexOf(space); - // : - if (span[scopeStart + 1] == colon) - { - scopeStart += 2; - // : - // ↑ - firstSpace = span[scopeStart..].IndexOf(space) + scopeStart; - startIndex = firstSpace + 1; - // : - // |_________| - length = span[startIndex..].IndexOf(space); - - if (firstSpace == scopeStart - 1 || length == -1) - return (IrcCommand.Unknown, 0); - - command = span[startIndex..(startIndex + length)]; - } - // : - else - { - // Since there are no tags, 'scopeStart' will be 'firstSpace' here - ++scopeStart; - int secondSpace = span[scopeStart..].IndexOf(space) + scopeStart; - if (secondSpace == scopeStart - 1) - secondSpace = span.Length; - - command = span[scopeStart..secondSpace]; - } - } - - return ((IrcCommand)command.Sum(), span.IndexOf(lf) + 1); - } -} diff --git a/MiniTwitch.Irc/IrcClient.cs b/MiniTwitch.Irc/IrcClient.cs index 71a844c..e16b5f9 100644 --- a/MiniTwitch.Irc/IrcClient.cs +++ b/MiniTwitch.Irc/IrcClient.cs @@ -5,7 +5,7 @@ using MiniTwitch.Irc.Interfaces; using MiniTwitch.Irc.Internal; using MiniTwitch.Irc.Internal.Enums; -using MiniTwitch.Irc.Internal.Parsing; +using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Models; namespace MiniTwitch.Irc; @@ -146,7 +146,7 @@ public sealed class IrcClient : IAsyncDisposable #endregion #region Fields - private static readonly WaitableEvents[] _channelJoinEvents = new[] + private static readonly WaitableEvents[] _channelJoinEvents = new[] { WaitableEvents.JoinedChannel, WaitableEvents.ChannelSuspended }; @@ -316,7 +316,7 @@ public async ValueTask SendMessage(string channel, string message, bool action = if (!_manager.CanSend(channel, _moderated.Contains(channel))) { - TimeSpan delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); + var delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); Log(LogLevel.Debug, "Cannot send message to #{channel}: Rate limit of {count} hit. Retrying in {delay}s", channel, this.Options.ModMessageRateLimit, delay.TotalSeconds); Log(LogLevel.Warning, "#{channel}: Your message was not sent yet due to the configured messaging ratelimit (normal: {normal}/30s, mod: {mod}/30s)", @@ -355,7 +355,7 @@ public async ValueTask ReplyTo(Privmsg parentMessage, string message, bool actio string channel = parentMessage.Channel.Name; if (!_manager.CanSend(channel, _moderated.Contains(channel))) { - TimeSpan delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); + var delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); Log(LogLevel.Debug, "Cannot send message to #{channel}: Rate limit of {count} hit. Retrying in {delay}s", channel, this.Options.ModMessageRateLimit, delay.TotalSeconds); Log(LogLevel.Warning, "#{channel}: Your message was not sent yet due to the configured messaging ratelimit (normal: {normal}/30s, mod: {mod}/30s)", @@ -400,7 +400,7 @@ public async ValueTask ReplyTo(string messageId, string channel, string reply, b if (!_manager.CanSend(channel, _moderated.Contains(channel))) { - TimeSpan delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); + var delay = TimeSpan.FromSeconds(90 / this.Options.MessageRateLimit); Log(LogLevel.Debug, "Cannot send message to #{channel}: Rate limit of {count} hit. Retrying in {delay}s", channel, this.Options.ModMessageRateLimit, delay.TotalSeconds); Log(LogLevel.Warning, "#{channel}: Your message was not sent yet due to the configured messaging ratelimit (normal: {normal}/30s, mod: {mod}/30s)", @@ -527,22 +527,18 @@ public Task PartChannel(string channel, CancellationToken cancellationToken = de #region Parsing internal void Parse(ReadOnlyMemory data) { - (IrcCommand command, int lfIndex) = IrcParsing.ParseCommand(data.Span); - int accumulatedIndex = lfIndex; - ReceiveData(command, data[..(lfIndex == 0 ? ^0 : lfIndex - 2)]); - // Go over data if it has multiple messages - while (lfIndex != 0 && data.Length - accumulatedIndex > 0) + IrcMessage message = new(data); + HandleMessage(ref message); + if (message.IsMultipleMessages) { - (command, lfIndex) = IrcParsing.ParseCommand(data.Span[accumulatedIndex..]); - ReceiveData(command, data[accumulatedIndex..(lfIndex == 0 ? ^0 : lfIndex + accumulatedIndex - 2)]); - accumulatedIndex += lfIndex; + Parse(data[message.NextMessageStartIndex..]); } } - private void ReceiveData(IrcCommand command, ReadOnlyMemory data) + private void HandleMessage(ref IrcMessage message) { // Return if user decides to ignore the command - if (this.Options.IgnoreCommands != IgnoreCommand.None && command switch + if (this.Options.IgnoreCommands != IgnoreCommand.None && message.Command switch { IrcCommand.PRIVMSG => this.Options.IgnoreCommands.HasFlag(IgnoreCommand.PRIVMSG), IrcCommand.USERNOTICE => this.Options.IgnoreCommands.HasFlag(IgnoreCommand.USERNOTICE), @@ -560,10 +556,10 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) return; } - switch (command) + switch (message.Command) { case IrcCommand.PRIVMSG: - Privmsg ircMessage = new(data, this); + Privmsg ircMessage = new(ref message, this); OnMessage?.Invoke(ircMessage).StepOver(this.ExceptionHandler); break; @@ -591,7 +587,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.USERNOTICE: - Usernotice usernotice = new(data); + Usernotice usernotice = new(ref message); switch (usernotice.MsgId) { case UsernoticeType.Sub @@ -628,7 +624,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.CLEARCHAT: - Clearchat clearchat = new(data); + Clearchat clearchat = new(ref message); if (clearchat.IsClearChat) OnChatClear?.Invoke(clearchat).StepOver(this.ExceptionHandler); else if (clearchat.IsBan) @@ -639,12 +635,12 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.CLEARMSG: - Clearmsg clearmsg = new(data); + Clearmsg clearmsg = new(ref message); OnMessageDelete?.Invoke(clearmsg).StepOver(this.ExceptionHandler); break; case IrcCommand.ROOMSTATE: - IrcChannel ircChannel = new(data); + IrcChannel ircChannel = new(ref message); if (ircChannel.Roomstate == RoomstateType.All) { _coordinator.ReleaseIfLocked(WaitableEvents.JoinedChannel); @@ -685,7 +681,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.PART: - IrcChannel channel = new(data); + IrcChannel channel = new(ref message); if (this.JoinedChannels.Remove(channel)) { Log(LogLevel.Information, "Parted #{channel}", channel.Name); @@ -697,7 +693,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.NOTICE: - Notice notice = new(data); + Notice notice = new(ref message); if (notice.Type == NoticeType.Msg_channel_suspended) { Log(LogLevel.Error, "Tried joining suspended channel: #{channel}", notice.Channel.Name); @@ -712,7 +708,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.USERSTATE or IrcCommand.GLOBALUSERSTATE: - Userstate state = new(data); + Userstate state = new(ref message); if (state.Self.IsMod && !_moderated.Contains(state.Channel.Name)) _ = _moderated.Add(state.Channel.Name); @@ -720,7 +716,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) break; case IrcCommand.WHISPER: - Whisper whisper = new(data); + Whisper whisper = new(ref message); OnWhisper?.Invoke(whisper).StepOver(this.ExceptionHandler); break; } @@ -728,7 +724,7 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) #endregion #region Utils - private ILogger GetLogger() => this.Options.Logger ?? DefaultLogger; + private ILogger GetLogger() => this.Options.Logger ?? this.DefaultLogger; private void LogEventException(Exception ex) => LogException(ex, "🚨 Exception caught in an event:"); diff --git a/MiniTwitch.Irc/IrcMembershipClient.cs b/MiniTwitch.Irc/IrcMembershipClient.cs index 80b3704..a46a28a 100644 --- a/MiniTwitch.Irc/IrcMembershipClient.cs +++ b/MiniTwitch.Irc/IrcMembershipClient.cs @@ -4,7 +4,7 @@ using MiniTwitch.Irc.Interfaces; using MiniTwitch.Irc.Internal; using MiniTwitch.Irc.Internal.Enums; -using MiniTwitch.Irc.Internal.Parsing; +using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Models; namespace MiniTwitch.Irc; @@ -234,20 +234,17 @@ public Task PartChannel(string channel, CancellationToken cancellationToken = de #region Parsing internal void Parse(ReadOnlyMemory data) { - (IrcCommand command, int lfIndex) = IrcParsing.ParseCommand(data.Span); - int accumulatedIndex = lfIndex; - ReceiveData(command, data); - while (lfIndex != 0 && data.Length - accumulatedIndex > 0) + IrcMessage message = new(data); + HandleMessage(message); + if (message.IsMultipleMessages) { - (command, lfIndex) = IrcParsing.ParseCommand(data.Span[accumulatedIndex..]); - ReceiveData(command, data[accumulatedIndex..]); - accumulatedIndex += lfIndex; + Parse(data[message.NextMessageStartIndex..]); } } - private void ReceiveData(IrcCommand command, ReadOnlyMemory data) + private void HandleMessage(IrcMessage message) { - switch (command) + switch (message.Command) { case IrcCommand.Connected: if (_connectionWaiter.CurrentCount == 0) @@ -278,11 +275,11 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) { User = new MessageAuthor() { - Name = data.Span.FindUsername(noTags: true) + Name = message.GetUsername() }, Channel = new IrcChannel() { - Name = data.Span.FindChannel(anySeparator: true) + Name = message.GetChannel() } }; OnUserJoin?.Invoke(joinArgs).StepOver(this.ExceptionHandler); @@ -293,11 +290,11 @@ private void ReceiveData(IrcCommand command, ReadOnlyMemory data) { User = new MessageAuthor() { - Name = data.Span.FindUsername(noTags: true) + Name = message.GetUsername() }, Channel = new IrcChannel() { - Name = data.Span.FindChannel(anySeparator: true) + Name = message.GetChannel() } }; OnUserPart?.Invoke(partArgs).StepOver(this.ExceptionHandler); diff --git a/MiniTwitch.Irc/Models/ClearChat.cs b/MiniTwitch.Irc/Models/ClearChat.cs index f7a525d..e0a6d29 100644 --- a/MiniTwitch.Irc/Models/ClearChat.cs +++ b/MiniTwitch.Irc/Models/ClearChat.cs @@ -1,6 +1,7 @@ using System.Text; using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -30,7 +31,7 @@ namespace MiniTwitch.Irc.Models; internal bool IsClearChat { get; init; } internal bool IsBan { get; init; } - internal Clearchat(ReadOnlyMemory memory) + internal Clearchat(ref IrcMessage message) { int duration = 0; long targetId = 0; @@ -38,7 +39,7 @@ internal Clearchat(ReadOnlyMemory memory) long tmiSentTs = 0; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -47,22 +48,22 @@ internal Clearchat(ReadOnlyMemory memory) switch (tagKey.Sum()) { //room-id - case 695: + case (int)Tags.RoomId: channelId = TagHelper.GetLong(tagValue); break; //tmi-sent-ts - case 1093: + case (int)Tags.TmiSentTs: tmiSentTs = TagHelper.GetLong(tagValue); break; //ban-duration - case 1220: + case (int)Tags.BanDuration: duration = TagHelper.GetInt(tagValue); break; //target-user-id - case 1389: + case (int)Tags.TargetUserId: targetId = TagHelper.GetLong(tagValue); break; } @@ -71,12 +72,12 @@ internal Clearchat(ReadOnlyMemory memory) this.Duration = duration == 0 ? TimeSpan.Zero : TimeSpan.FromSeconds(duration); this.Target = new MessageAuthor() { - Name = memory.Span.FindContent().Content, + Name = message.HasMessageContent ? message.GetContent().Content : string.Empty, Id = targetId }; this.Channel = new IrcChannel() { - Name = memory.Span.FindChannel(), + Name = message.GetChannel(), Id = channelId }; this.TmiSentTs = tmiSentTs; @@ -92,6 +93,7 @@ internal Clearchat(ReadOnlyMemory memory) public static Clearchat Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } } diff --git a/MiniTwitch.Irc/Models/Clearmsg.cs b/MiniTwitch.Irc/Models/Clearmsg.cs index 7dedc4d..689fa98 100644 --- a/MiniTwitch.Irc/Models/Clearmsg.cs +++ b/MiniTwitch.Irc/Models/Clearmsg.cs @@ -1,6 +1,7 @@ using System.Text; using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -31,14 +32,14 @@ namespace MiniTwitch.Irc.Models; /// public DateTimeOffset SentTimestamp => DateTimeOffset.FromUnixTimeMilliseconds(this.TmiSentTs); - internal Clearmsg(ReadOnlyMemory memory) + internal Clearmsg(ref IrcMessage message) { string targetUsername = string.Empty; - string channelName = memory.Span.FindChannel(); + string channelName = message.GetChannel(); string messageId = string.Empty; long tmiSentTs = 0; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -47,17 +48,17 @@ internal Clearmsg(ReadOnlyMemory memory) switch (tagKey.Sum()) { //login - case 537: + case (int)Tags.Login: targetUsername = TagHelper.GetString(tagValue); break; //tmi-sent-ts - case 1093: + case (int)Tags.TmiSentTs: tmiSentTs = TagHelper.GetLong(tagValue); break; //target-msg-id - case 1269: + case (int)Tags.TargetMsgId: messageId = TagHelper.GetString(tagValue); break; @@ -73,7 +74,7 @@ internal Clearmsg(ReadOnlyMemory memory) Name = channelName }; this.MessageId = messageId; - this.MessageContent = memory.Span.FindContent(maybeAction: true).Content; + this.MessageContent = message.GetContent(maybeAction: true).Content; this.TmiSentTs = tmiSentTs; } @@ -85,7 +86,8 @@ internal Clearmsg(ReadOnlyMemory memory) public static Clearmsg Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } /// diff --git a/MiniTwitch.Irc/Models/IrcChannel.cs b/MiniTwitch.Irc/Models/IrcChannel.cs index 7dbbfcb..34780dc 100644 --- a/MiniTwitch.Irc/Models/IrcChannel.cs +++ b/MiniTwitch.Irc/Models/IrcChannel.cs @@ -37,12 +37,12 @@ namespace MiniTwitch.Irc.Models; private static readonly TimeSpan _followersOnlyOffTimeSpan = TimeSpan.FromMinutes(-1); - internal IrcChannel(ReadOnlyMemory memory) + internal IrcChannel(ref IrcMessage message) { int followerModeDuration = -1; int slowModeDuration = 0; // do this otherwise it will include '\r\n' in the name - string name = memory.Span.FindChannel(anySeparator: true); + string name = message.GetChannel(); long id = 0; bool emoteOnlyModified = false; @@ -51,7 +51,7 @@ internal IrcChannel(ReadOnlyMemory memory) bool followerModeModified = false; bool slowModeModified = false; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -59,36 +59,36 @@ internal IrcChannel(ReadOnlyMemory memory) switch (tagKey.Sum()) { //r9k - case 278: + case (int)Tags.R9K: this.UniqueModeEnabled = TagHelper.GetBool(tagValue); uniqueModeModified = true; break; //slow - case 453: + case (int)Tags.Slow: slowModeDuration = TagHelper.GetInt(tagValue); slowModeModified = true; break; //room-id - case 695: + case (int)Tags.RoomId: id = TagHelper.GetLong(tagValue); break; //subs-only - case 940: + case (int)Tags.SubsOnly: this.SubOnlyEnabled = TagHelper.GetBool(tagValue); subModeModified = true; break; //emote-only - case 1033: + case (int)Tags.EmoteOnly: this.EmoteOnlyEnabled = TagHelper.GetBool(tagValue); emoteOnlyModified = true; break; //followers-only - case 1484: + case (int)Tags.FollowersOnly: followerModeDuration = TagHelper.GetInt(tagValue); followerModeModified = true; break; @@ -140,7 +140,8 @@ internal IrcChannel(ReadOnlyMemory memory) public static IrcChannel Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } #pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). diff --git a/MiniTwitch.Irc/Models/MessageReply.cs b/MiniTwitch.Irc/Models/MessageReply.cs index da40645..0f6ccba 100644 --- a/MiniTwitch.Irc/Models/MessageReply.cs +++ b/MiniTwitch.Irc/Models/MessageReply.cs @@ -38,7 +38,7 @@ public readonly struct MessageReply /// /// Whether there are reply contents in this message /// - public bool HasContent => ParentUserId != 0; + public bool HasContent => this.ParentUserId != 0; /// public static implicit operator string(MessageReply messageReply) => messageReply.HasContent ? messageReply.ParentMessage : string.Empty; diff --git a/MiniTwitch.Irc/Models/Notice.cs b/MiniTwitch.Irc/Models/Notice.cs index 2500ec1..9cea2cd 100644 --- a/MiniTwitch.Irc/Models/Notice.cs +++ b/MiniTwitch.Irc/Models/Notice.cs @@ -1,7 +1,9 @@ -using System.Text; +using System; +using System.Text; using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Enums; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -25,10 +27,10 @@ namespace MiniTwitch.Irc.Models; /// public NoticeType Type { get; init; } = NoticeType.Unknown; - internal Notice(ReadOnlyMemory memory) + internal Notice(ref IrcMessage message) { - this.SystemMessage = memory.Span.FindContent().Content; - using IrcTags ircTags = IrcParsing.ParseTags(memory); + this.SystemMessage = message.GetContent().Content; + using IrcTags ircTags = message.ParseTags(); foreach (IrcTag tag in ircTags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -38,24 +40,16 @@ internal Notice(ReadOnlyMemory memory) switch (tagKey.Sum()) { //msg-id - case 577: - // I do not like this. But because some of its values would have the same sum, I'm forced to do this + case (int)Tags.MsgId: this.Type = TagHelper.GetEnum(tagValue); break; } } - try + this.Channel = new IrcChannel() { - this.Channel = new IrcChannel() - { - Name = memory.Span.FindChannel() - }; - } - catch - { - this.Type = NoticeType.Bad_auth; - } + Name = message.IsGlobalChannel ? "*" : message.GetChannel() + }; } /// @@ -66,7 +60,8 @@ internal Notice(ReadOnlyMemory memory) public static Notice Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } #pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). diff --git a/MiniTwitch.Irc/Models/Privmsg.cs b/MiniTwitch.Irc/Models/Privmsg.cs index cb7925e..3b63efc 100644 --- a/MiniTwitch.Irc/Models/Privmsg.cs +++ b/MiniTwitch.Irc/Models/Privmsg.cs @@ -4,6 +4,7 @@ using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Enums; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -83,7 +84,7 @@ namespace MiniTwitch.Irc.Models; internal IrcClient? Source { get; init; } - internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) + internal Privmsg(ref IrcMessage message, IrcClient? source = null) { this.Source = source; @@ -92,7 +93,7 @@ internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) string badgeInfo = string.Empty; Color color = default; string displayName = string.Empty; - string username = memory.Span.FindUsername(); + string username = message.GetUsername(); long uid = 0; bool mod = false; bool sub = false; @@ -117,11 +118,10 @@ internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) HypeChatLevel level = HypeChatLevel.None; // IBasicChannel - string channelName = memory.Span.FindChannel(); + string channelName = message.GetChannel(); long channelId = 0; - (this.Content, this.IsAction) = memory.Span.FindContent(maybeAction: true); - + (this.Content, this.IsAction) = message.GetContent(maybeAction: true); string emotes = string.Empty; string flags = string.Empty; string id = string.Empty; @@ -131,7 +131,7 @@ internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) bool firstMsg = false; bool returningChatter = false; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -140,157 +140,157 @@ internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) switch (tagKey.Sum()) { //id - case 205: + case (int)Tags.Id: id = TagHelper.GetString(tagValue); break; //mod - case 320: + case (int)Tags.Mod: mod = TagHelper.GetBool(tagValue); break; //vip - case 335: + case (int)Tags.Vip: vip = true; break; //bits - case 434: + case (int)Tags.Bits: bits = TagHelper.GetInt(tagValue); break; //flags - case 525: + case (int)Tags.Flags: flags = TagHelper.GetString(tagValue); break; //color - case 543: + case (int)Tags.Color: color = TagHelper.GetColor(tagValue); break; //turbo - case 556: + case (int)Tags.Turbo: turbo = TagHelper.GetBool(tagValue); break; //badges - case 614: + case (int)Tags.Badges: badges = TagHelper.GetString(tagValue, true); break; //emotes - case 653: + case (int)Tags.Emotes: emotes = TagHelper.GetString(tagValue); break; //room-id - case 695: + case (int)Tags.RoomId: channelId = TagHelper.GetLong(tagValue); break; //user-id - case 697: + case (int)Tags.UserId: uid = TagHelper.GetLong(tagValue); break; //first-msg - case 924: + case (int)Tags.FirstMsg: firstMsg = TagHelper.GetBool(tagValue); break; //user-type - case 942 when tagValue.Length > 0: + case (int)Tags.UserType when tagValue.Length > 0: userType = (UserType)tagValue.Sum(); break; //badge-info - case 972: + case (int)Tags.BadgeInfo: badgeInfo = TagHelper.GetString(tagValue, true, true); break; //subscriber - case 1076: + case (int)Tags.Subscriber: sub = TagHelper.GetBool(tagValue); break; //tmi-sent-ts - case 1093: + case (int)Tags.TmiSentTs: tmiSentTs = TagHelper.GetLong(tagValue); break; //client-nonce - case 1215: + case (int)Tags.ClientNonce: nonce = TagHelper.GetString(tagValue); break; //display-name - case 1220: + case (int)Tags.DisplayName: displayName = TagHelper.GetString(tagValue); break; //returning-chatter - case 1782: + case (int)Tags.ReturningChatter: returningChatter = TagHelper.GetBool(tagValue); break; //reply-parent-msg-id - case 1873: + case (int)Tags.ReplyParentMsgId: replyMessageId = TagHelper.GetString(tagValue); break; //reply-parent-user-id - case 1993: + case (int)Tags.ReplyParentUserId: replyUserId = TagHelper.GetLong(tagValue); break; //reply-parent-msg-body - case 2098: + case (int)Tags.ReplyParentMsgBody: replyMessageBody = TagHelper.GetString(tagValue, unescape: true); break; //pinned-chat-paid-level - case 2139: + case (int)Tags.PinnedChatPaidLevel: level = TagHelper.GetEnum(tagValue); break; //pinned-chat-paid-amount - case 2263: + case (int)Tags.PinnedChatPaidAmount: paidAmount = TagHelper.GetInt(tagValue); break; //reply-parent-user-login - case 2325: + case (int)Tags.ReplyParentUserLogin: replyUsername = TagHelper.GetString(tagValue); break; //pinned-chat-paid-currency - case 2478: + case (int)Tags.PinnedChatPaidCurrency: currency = TagHelper.GetString(tagValue, intern: true); break; //pinned-chat-paid-exponent - case 2484: + case (int)Tags.PinnedChatPaidExponent: exponent = TagHelper.GetInt(tagValue); break; //reply-parent-display-name - case 2516: + case (int)Tags.ReplyParentDisplayName: replyDisplayName = TagHelper.GetString(tagValue); break; //reply-thread-parent-msg-id - case 2550: + case (int)Tags.ReplyThreadParentMsgId: threadParentMessageid = TagHelper.GetString(tagValue); break; //reply-thread-parent-user-login - case 3002: + case (int)Tags.ReplyThreadParentUserLogin: threadParentUsername = TagHelper.GetString(tagValue); break; //pinned-chat-paid-is-system-message - case 3331: + case (int)Tags.PinnedChatPaidIsSystemMessage: isSystemMessage = TagHelper.GetBool(tagValue); break; } @@ -350,7 +350,7 @@ internal Privmsg(ReadOnlyMemory memory, IrcClient? source = null) /// Prepend .me /// Prefer replying to the target message in the same thread instead of creating a new one /// A cancellation token to stop further execution of asynchronous actions - public ValueTask ReplyWith(string reply, bool action = false, bool replyInThread = false, CancellationToken cancellationToken = default) => + public ValueTask ReplyWith(string reply, bool action = false, bool replyInThread = false, CancellationToken cancellationToken = default) => this.Source?.ReplyTo(this, reply, action, replyInThread, cancellationToken) ?? ValueTask.CompletedTask; /// @@ -361,7 +361,8 @@ public ValueTask ReplyWith(string reply, bool action = false, bool replyInThread public static Privmsg Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } /// diff --git a/MiniTwitch.Irc/Models/Usernotice.cs b/MiniTwitch.Irc/Models/Usernotice.cs index 07f44b4..b59384f 100644 --- a/MiniTwitch.Irc/Models/Usernotice.cs +++ b/MiniTwitch.Irc/Models/Usernotice.cs @@ -68,7 +68,7 @@ namespace MiniTwitch.Irc.Models; internal UsernoticeType MsgId { get; init; } = UsernoticeType.None; - internal Usernotice(ReadOnlyMemory memory) + internal Usernotice(ref IrcMessage message) { // Author bool isMod = false; @@ -88,7 +88,7 @@ internal Usernotice(ReadOnlyMemory memory) long recipientId = 0; // Channel - string channelName = memory.Span.FindChannel(); + string channelName = message.GetChannel(); long channelId = 0; SubPlan subPlan = SubPlan.None; @@ -98,7 +98,7 @@ internal Usernotice(ReadOnlyMemory memory) string id = string.Empty; string subPlanName = string.Empty; string systemMessage = string.Empty; - string message = string.Empty; + string content = string.Empty; string gifterUsername = string.Empty; string gifterDisplayName = string.Empty; int cumulativeMonths = 0; @@ -110,7 +110,7 @@ internal Usernotice(ReadOnlyMemory memory) int viewerCount = 0; bool shouldShareStreak = false; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -119,167 +119,167 @@ internal Usernotice(ReadOnlyMemory memory) switch (tagKey.Sum()) { //id - case 205: + case (int)Tags.Id: id = TagHelper.GetString(tagValue); break; //mod - case 320: + case (int)Tags.Mod: isMod = TagHelper.GetBool(tagValue); break; //flags - case 525: + case (int)Tags.Flags: flags = TagHelper.GetString(tagValue); break; //login - case 537: + case (int)Tags.Login: username = TagHelper.GetString(tagValue); break; //color - case 543: + case (int)Tags.Color: colorCode = TagHelper.GetColor(tagValue); break; //turbo - case 556: + case (int)Tags.Turbo: isTurbo = TagHelper.GetBool(tagValue); break; //msg-id - case 577: + case (int)Tags.MsgId: this.MsgId = (UsernoticeType)tagValue.Sum(); break; //badges - case 614: + case (int)Tags.Badges: badges = TagHelper.GetString(tagValue, true); break; //emotes - case 653: + case (int)Tags.Emotes: emotes = TagHelper.GetString(tagValue); break; //room-id - case 695: + case (int)Tags.RoomId: channelId = TagHelper.GetLong(tagValue); break; //user-id - case 697: + case (int)Tags.UserId: userId = TagHelper.GetLong(tagValue); break; //user-type - case 942 when tagValue.Length > 0: + case (int)Tags.UserType when tagValue.Length > 0: userType = (UserType)tagValue.Sum(); break; //badge-info - case 972: + case (int)Tags.BadgeInfo: badgeInfo = TagHelper.GetString(tagValue, true, true); break; //system-msg - case 1049: + case (int)Tags.SystemMsg: systemMessage = TagHelper.GetString(tagValue, unescape: true); break; //subscriber - case 1076: + case (int)Tags.Subscriber: isSubscriber = TagHelper.GetBool(tagValue); break; //tmi-sent-ts - case 1093: + case (int)Tags.TmiSentTs: this.TmiSentTs = TagHelper.GetLong(tagValue); break; //display-name - case 1220: + case (int)Tags.DisplayName: displayName = TagHelper.GetString(tagValue); break; //msg-param-color - case 1489: + case (int)Tags.MsgParamColor: color = (AnnouncementColor)tagValue.Sum(); break; //msg-param-months - case 1611: + case (int)Tags.MsgParamMonths: months = TagHelper.GetInt(tagValue); break; //msg-param-sub-plan - case 1748: + case (int)Tags.MsgParamSubPlan: subPlan = (SubPlan)tagValue.Sum(); break; //msg-param-sender-name - case 2049: + case (int)Tags.MsgParamSenderName: gifterDisplayName = TagHelper.GetString(tagValue); break; //msg-param-gift-months - case 2082: + case (int)Tags.MsgParamGiftMonths: giftedMonths = TagHelper.GetInt(tagValue); break; //msg-param-viewerCount - case 2125: + case (int)Tags.MsgParamViewerCount: viewerCount = TagHelper.GetInt(tagValue); break; //msg-param-recipient-id - case 2159: + case (int)Tags.MsgParamRecipientId: recipientId = TagHelper.GetLong(tagValue); break; //msg-param-sender-login - case 2169: + case (int)Tags.MsgParamSenderLogin: gifterUsername = TagHelper.GetString(tagValue); break; //msg-param-sender-count - case 2185: + case (int)Tags.MsgParamSenderCount: totalGiftCount = TagHelper.GetInt(tagValue); break; //msg-param-sub-plan-name - case 2210: + case (int)Tags.MsgParamSubPlanName: subPlanName = TagHelper.GetString(tagValue, true, true); break; //msg-param-streak-months - case 2306: + case (int)Tags.MsgParamStreakMonths: monthStreak = TagHelper.GetInt(tagValue); break; //msg-param-mass-gift-count - case 2451: + case (int)Tags.MsgParamMassGiftCount: giftCount = TagHelper.GetInt(tagValue); break; //msg-param-cumulative-months - case 2743: + case (int)Tags.MsgParamCumulativeMonths: cumulativeMonths = TagHelper.GetInt(tagValue); break; //msg-param-recipient-user-name - case 2863: + case (int)Tags.MsgParamRecipientUserName: recipientUsername = TagHelper.GetString(tagValue); break; //msg-param-should-share-streak - case 2872: + case (int)Tags.MsgParamShouldShareStreak: shouldShareStreak = TagHelper.GetBool(tagValue); break; //msg-param-recipient-display-name - case 3174: + case (int)Tags.MsgparamRecipientDisplayName: recipientDisplayName = TagHelper.GetString(tagValue); break; } @@ -287,7 +287,7 @@ internal Usernotice(ReadOnlyMemory memory) if (this.MsgId is UsernoticeType.Resub or UsernoticeType.Announcement) { - message = memory.Span.FindContent(maybeEmpty: true).Content; + content = message.HasMessageContent ? message.GetContent().Content : string.Empty; } this.Author = new MessageAuthor() @@ -322,7 +322,7 @@ internal Usernotice(ReadOnlyMemory memory) this.Id = id; this.SubPlanName = subPlanName; this.SystemMessage = systemMessage; - this.Message = message; + this.Message = content; this.GifterUsername = gifterUsername; this.GifterDisplayName = gifterDisplayName; this.CumulativeMonths = cumulativeMonths; @@ -343,7 +343,8 @@ internal Usernotice(ReadOnlyMemory memory) public static Usernotice Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } #pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). diff --git a/MiniTwitch.Irc/Models/Userstate.cs b/MiniTwitch.Irc/Models/Userstate.cs index 2b1580a..370244c 100644 --- a/MiniTwitch.Irc/Models/Userstate.cs +++ b/MiniTwitch.Irc/Models/Userstate.cs @@ -3,6 +3,7 @@ using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Enums; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -37,7 +38,7 @@ public readonly struct Userstate internal IrcClient? Source { get; init; } - internal Userstate(ReadOnlyMemory memory, IrcClient? source = null) + internal Userstate(ref IrcMessage message, IrcClient? source = null) { this.Source = source; @@ -50,11 +51,11 @@ internal Userstate(ReadOnlyMemory memory, IrcClient? source = null) bool subscriber = false; bool turbo = false; UserType type = UserType.None; - string channel = memory.Span.FindChannel(true); + string channel = message.GetChannel(); string emoteSets = string.Empty; string nonce = string.Empty; - using IrcTags tags = IrcParsing.ParseTags(memory); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { if (tag.Key.Length == 0) @@ -68,57 +69,57 @@ internal Userstate(ReadOnlyMemory memory, IrcClient? source = null) { //mod - case 320: + case (int)Tags.Mod: mod = TagHelper.GetBool(tagValue); break; //vip - case 335: + case (int)Tags.Vip: vip = TagHelper.GetBool(tagValue); break; //color - case 543: + case (int)Tags.Color: color = TagHelper.GetColor(tagValue); break; //turbo - case 556: + case (int)Tags.Turbo: turbo = TagHelper.GetBool(tagValue); break; //badges - case 614: + case (int)Tags.Badges: badges = TagHelper.GetString(tagValue, true); break; //user-type - case 942 when tagValue.Length > 0: + case (int)Tags.UserType when tagValue.Length > 0: type = (UserType)tagValue.Sum(); break; //badge-info - case 972: + case (int)Tags.BadgeInfo: badgeInfo = TagHelper.GetString(tagValue, true, true); break; //emote-sets - case 1030: + case (int)Tags.EmoteSets: emoteSets = TagHelper.GetString(tagValue, true); break; //subscriber - case 1076: + case (int)Tags.Subscriber: subscriber = TagHelper.GetBool(tagValue); break; //client-nonce - case 1215: + case (int)Tags.ClientNonce: nonce = TagHelper.GetString(tagValue); break; //display-name - case 1220: + case (int)Tags.DisplayName: displayName = TagHelper.GetString(tagValue); break; } @@ -153,6 +154,7 @@ internal Userstate(ReadOnlyMemory memory, IrcClient? source = null) public static Userstate Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } } \ No newline at end of file diff --git a/MiniTwitch.Irc/Models/Whisper.cs b/MiniTwitch.Irc/Models/Whisper.cs index 8afcfb5..d94cdaa 100644 --- a/MiniTwitch.Irc/Models/Whisper.cs +++ b/MiniTwitch.Irc/Models/Whisper.cs @@ -3,6 +3,7 @@ using MiniTwitch.Common.Extensions; using MiniTwitch.Irc.Enums; using MiniTwitch.Irc.Interfaces; +using MiniTwitch.Irc.Internal.Enums; using MiniTwitch.Irc.Internal.Models; using MiniTwitch.Irc.Internal.Parsing; @@ -34,12 +35,12 @@ public readonly struct Whisper /// public bool IsAction { get; init; } - internal Whisper(ReadOnlyMemory memory) + internal Whisper(ref IrcMessage message) { string badges = string.Empty; Color color = default; string displayName = string.Empty; - string username = memory.Span.FindUsername(); + string username = message.GetUsername(); long uid = 0; UserType type = UserType.None; bool turbo = false; @@ -47,9 +48,8 @@ internal Whisper(ReadOnlyMemory memory) string emotes = string.Empty; int id = 0; string threadId = string.Empty; - (string content, bool action) = memory.Span.FindContent(maybeAction: true); - - using IrcTags tags = IrcParsing.ParseTags(memory); + (string content, bool action) = message.GetContent(maybeAction: true); + using IrcTags tags = message.ParseTags(); foreach (IrcTag tag in tags) { ReadOnlySpan tagKey = tag.Key.Span; @@ -57,47 +57,47 @@ internal Whisper(ReadOnlyMemory memory) switch (tagKey.Sum()) { //color - case 543: + case (int)Tags.Color: color = TagHelper.GetColor(tagValue); break; //turbo - case 556: + case (int)Tags.Turbo: turbo = TagHelper.GetBool(tagValue); break; //badges - case 614: + case (int)Tags.Badges: badges = TagHelper.GetString(tagValue, true); break; //emotes - case 653: + case (int)Tags.Emotes: emotes = TagHelper.GetString(tagValue); break; //user-id - case 697: + case (int)Tags.UserId: uid = TagHelper.GetLong(tagValue); break; //thread-id - case 882: + case (int)Tags.ThreadId: threadId = TagHelper.GetString(tagValue, true); break; //user-type - case 942 when tagValue.Length > 0: + case (int)Tags.UserType when tagValue.Length > 0: type = (UserType)tagValue.Sum(); break; //message-id - case 991: + case (int)Tags.MessageId: id = TagHelper.GetInt(tagValue); break; //display-name - case 1220: + case (int)Tags.DisplayName: displayName = TagHelper.GetString(tagValue); break; } @@ -128,7 +128,8 @@ internal Whisper(ReadOnlyMemory memory) public static Whisper Construct(string rawData) { ReadOnlyMemory memory = new(Encoding.UTF8.GetBytes(rawData)); - return new(memory); + var message = new IrcMessage(memory); + return new(ref message); } /// diff --git a/MiniTwitch.sln b/MiniTwitch.sln index ac681bc..b9f0ba7 100644 --- a/MiniTwitch.sln +++ b/MiniTwitch.sln @@ -20,9 +20,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C1BCE870-D15 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1AE9EDC5-22F2-4EEB-9B8A-46B264597907}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniTwitch.PubSub", "MiniTwitch.PubSub\MiniTwitch.PubSub.csproj", "{91F7B7DD-2061-470C-BCF0-649CD5FEFDA5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniTwitch.PubSub", "MiniTwitch.PubSub\MiniTwitch.PubSub.csproj", "{91F7B7DD-2061-470C-BCF0-649CD5FEFDA5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniTwitch.PubSub.Test", "MiniTwitch.PubSub.Test\MiniTwitch.PubSub.Test.csproj", "{F7E72622-6545-46D4-92A4-493849E0DFCD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniTwitch.PubSub.Test", "MiniTwitch.PubSub.Test\MiniTwitch.PubSub.Test.csproj", "{F7E72622-6545-46D4-92A4-493849E0DFCD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{3AB3F64E-4D4B-4E24-862F-54E734F5A401}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniTwitch.Irc.Benchmarks", "MiniTwitch.Irc.Benchmarks\MiniTwitch.Irc.Benchmarks.csproj", "{E6F90853-811D-4E89-A911-14325A52ECE8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,6 +54,10 @@ Global {F7E72622-6545-46D4-92A4-493849E0DFCD}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7E72622-6545-46D4-92A4-493849E0DFCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7E72622-6545-46D4-92A4-493849E0DFCD}.Release|Any CPU.Build.0 = Release|Any CPU + {E6F90853-811D-4E89-A911-14325A52ECE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6F90853-811D-4E89-A911-14325A52ECE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6F90853-811D-4E89-A911-14325A52ECE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6F90853-811D-4E89-A911-14325A52ECE8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +68,7 @@ Global {70E0798C-2704-4905-AE1E-710136EC5C58} = {1AE9EDC5-22F2-4EEB-9B8A-46B264597907} {91F7B7DD-2061-470C-BCF0-649CD5FEFDA5} = {C1BCE870-D158-438A-91C6-387B2F7677B4} {F7E72622-6545-46D4-92A4-493849E0DFCD} = {1AE9EDC5-22F2-4EEB-9B8A-46B264597907} + {E6F90853-811D-4E89-A911-14325A52ECE8} = {3AB3F64E-4D4B-4E24-862F-54E734F5A401} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2698C4F9-D2CF-447D-945B-6382FF15EA63}