Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite IrcParser, pass through raw text to IrcMessage and fix other issues #230

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ UpgradeLog*.htm
# Microsoft Fakes
FakesAssemblies/

# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts/

# =========================
# Operating System Files
# =========================
Expand Down Expand Up @@ -233,4 +236,4 @@ $RECYCLE.BIN/
*.msp

# Windows shortcuts
*.lnk
*.lnk
18 changes: 18 additions & 0 deletions TwitchLib.Client.Benchmark/IrcMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using BenchmarkDotNet.Attributes;
using TwitchLib.Client.Internal.Parsing;

namespace TwitchLib.Client.Benchmark
{
[MemoryDiagnoser]
public class IrcMessageHandlerBenchmark
{
[Benchmark]
public bool ParseAndCheckFailedAuth()
{
return IrcParser
.ParseMessage(@"@badges=subscriber/0;color=#0000FF;display-name=KittyJinxu;emotes=30259:0-6;id=1154b7c0-8923-464e-a66b-3ef55b1d4e50;login=kittyjinxu;mod=0;msg-id=ritual;msg-param-ritual-name=new_chatter;room-id=35740817;subscriber=1;system-msg=@KittyJinxu\sis\snew\shere.\sSay\shello!;tmi-sent-ts=1514387871555;turbo=0;user-id=187446639;user-type= USERNOTICE #thorlar kittyjinxu > #thorlar: HeyGuys")
.ToString()
.StartsWith(":tmi.twitch.tv NOTICE * :Login authentication failed");
}
}
}
23 changes: 23 additions & 0 deletions TwitchLib.Client.Benchmark/IrcParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using BenchmarkDotNet.Attributes;
using TwitchLib.Client.Internal.Parsing;
using TwitchLib.Client.Models.Internal;

namespace TwitchLib.Client.Benchmark
{
[MemoryDiagnoser]
public class IrcParserBenchmark
{
[Params(
"@msg-id=host_on :tmi.twitch.tv NOTICE #burkeblack :Now hosting DjTechlive.",
":blubott.tmi.twitch.tv 366 blubott #monstercat :End of /NAMES list",
":jtv!jtv@jtv.tmi.twitch.tv PRIVMSG (HOSTED):(HOSTER) is now hosting you for (VIEWERS_TOTAL) viewers.",
"@msg-id=raid_notice_mature :tmi.twitch.tv NOTICE #swiftyspiffy :This channel is intended for mature audiences.",
@"@badges=subscriber/0;color=#0000FF;display-name=KittyJinxu;emotes=30259:0-6;id=1154b7c0-8923-464e-a66b-3ef55b1d4e50;login=kittyjinxu;mod=0;msg-id=ritual;msg-param-ritual-name=new_chatter;room-id=35740817;subscriber=1;system-msg=@KittyJinxu\sis\snew\shere.\sSay\shello!;tmi-sent-ts=1514387871555;turbo=0;user-id=187446639;user-type= USERNOTICE #thorlar kittyjinxu > #thorlar: HeyGuys",
"@badge-info=subscriber/22;badges=subscriber/3012;color=#FFFF00;display-name=FELYP8;emote-only=1;emotes=521050:0-6,8-14,16-22,24-30,32-38,40-46,48-54,56-62,64-70,72-78,80-86,88-94,96-102,104-110,148-154,156-162,164-170,172-178,180-186,188-194,196-202,204-210,212-218,220-226,228-234,236-242,244-250,252-258,260-266/302827730:112-119/302827734:121-128/302827735:130-137/302827737:139-146;first-msg=0;flags=;id=1844235a-c24e-4e18-937b-805d6601aebe;mod=0;returning-chatter=0;room-id=22484632;subscriber=1;tmi-sent-ts=1685664001040;turbo=0;user-id=162760707;user-type= :felyp8!felyp8@felyp8.tmi.twitch.tv PRIVMSG #forsen :forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE1 forsenE2 forsenE3 forsenE4 forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE forsenE"
)]
public string? Message;

[Benchmark]
public IrcMessage Parse() => IrcParser.ParseMessage(Message!);
}
}
5 changes: 5 additions & 0 deletions TwitchLib.Client.Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using BenchmarkDotNet.Running;

BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(args);
20 changes: 20 additions & 0 deletions TwitchLib.Client.Benchmark/TwitchLib.Client.Benchmark.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TwitchLib.Client\TwitchLib.Client.csproj" />
</ItemGroup>

</Project>
66 changes: 51 additions & 15 deletions TwitchLib.Client.Models/Internal/IrcMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ public class IrcMessage
public string Channel => _channel ??= Params.StartsWith("#") ? Params.Remove(0, 1) : Params;
private string _channel;

public string Params => _parameters != null && _parameters.Length > 0 ? _parameters[0] : "";
public string Params => _parameters?.Length > 0 ? _parameters[0] : "";

/// <summary>
/// Message itself
/// </summary>
public string Message => Trailing;

public string Trailing => _parameters != null && _parameters.Length > 1 ? _parameters[_parameters.Length - 1] : "";
public string Trailing => _parameters?.Length > 1 ? _parameters[_parameters.Length - 1] : "";

/// <summary>
/// Command parameters
Expand Down Expand Up @@ -76,18 +76,51 @@ public IrcMessage(
Dictionary<string, string> tags = null)
{
var idx = hostmask.IndexOf('!');
User = idx != -1 ? hostmask.Substring(0, idx) : hostmask;
User = idx >= 0 ? hostmask.Substring(0, idx) : hostmask;
Hostmask = hostmask;
_parameters = parameters;
Command = command;
Tags = tags;

if (command == IrcCommand.RPL_353)
if (command == IrcCommand.RPL_353
&& Params.Length > 0
&& Params.Contains("#"))
{
if(Params.Length > 0 && Params.Contains("#"))
{
_parameters[0] = $"#{_parameters[0].Split('#')[1]}";
}
_parameters[0] = $"#{_parameters[0].Split('#')[1]}";
}
}

/// <summary>
/// Create an IrcMessage, settings its raw string.
/// IrcParser *must* use this constructor, otherwise the raw string
/// will be re-generated on each PRIVMSG (90% of all messages)
/// </summary>
/// <param name="raw">Raw IRC message</param>
/// <param name="command">IRC Command</param>
/// <param name="parameters">Command params</param>
/// <param name="user">User</param>
/// <param name="hostmask">Hostmask</param>
/// <param name="tags">IRCv3 tags</param>
internal IrcMessage(
string raw,
IrcCommand command,
string[] parameters,
string user,
string hostmask,
Dictionary<string, string> tags = null)
{
User = user;
Hostmask = hostmask;
Command = command;
Tags = tags;

_rawString = raw;
_parameters = parameters;
if (command == IrcCommand.RPL_353
&& Params.Length > 0
&& Params.Contains("#"))
{
_parameters[0] = $"#{_parameters[0].Split('#')[1]}";
}
}

Expand All @@ -96,10 +129,10 @@ public IrcMessage(

private string GenerateToString()
{
var raw = new StringBuilder(64);
var raw = new StringBuilder(128);
if (Tags?.Count > 0)
{
raw.Append("@");
raw.Append('@');
foreach (var tag in Tags)
{
raw.Append(tag.Key).Append('=').Append(tag.Value).Append(';');
Expand All @@ -109,24 +142,27 @@ private string GenerateToString()

if (!string.IsNullOrEmpty(Hostmask))
{
raw.Append(":").Append(Hostmask).Append(" ");
raw.Append(':').Append(Hostmask).Append(' ');
}

raw.Append(Command.ToString().ToUpper().Replace("RPL_", ""));
// The "RPL_" replace is required because TwitchLib.Client.Enums.Internal.IrcCommand
// has the RPL_ prefix on all RPL commands, but the Twitch IRCv3 spec does not.
// Thus, if the message has not been constructed with _rawString from incoming data, remove the prefix.
raw.Append(Command.ToString().ToUpperInvariant().Replace("RPL_", ""));
if (_parameters == null || _parameters.Length == 0)
return raw.ToString();

if (_parameters[0] != null && _parameters[0].Length > 0)
if (!string.IsNullOrEmpty(_parameters[0]))
{
raw.Append(" ").Append(_parameters[0]);
}

if (_parameters.Length > 1 && _parameters[1] != null && _parameters[1].Length > 0)
if (_parameters.Length > 1 && _parameters[1]?.Length > 0)
{
raw.Append(" :").Append(_parameters[1]);
}

return raw.ToString();
}
}
}
}
6 changes: 6 additions & 0 deletions TwitchLib.Client.Models/TwitchLib.Client.Models.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@
<ItemGroup>
<ProjectReference Include="..\TwitchLib.Client.Enums\TwitchLib.Client.Enums.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<!-- This is a punishment for worshiping wrong gods of abstraction -->
<_Parameter1>TwitchLib.Client</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
3 changes: 1 addition & 2 deletions TwitchLib.Client.Test/TwitchLib.Client.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
6 changes: 6 additions & 0 deletions TwitchLib.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7495B75C-C41
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8C69E628-6CFC-4F25-8CAC-EC63927C0120}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitchLib.Client.Benchmark", "TwitchLib.Client.Benchmark\TwitchLib.Client.Benchmark.csproj", "{7E666AC9-B717-4E00-A3CB-16950275D3FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,6 +39,10 @@ Global
{4E8210DE-33A6-42B0-89F9-ED84A8AC197C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E8210DE-33A6-42B0-89F9-ED84A8AC197C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E8210DE-33A6-42B0-89F9-ED84A8AC197C}.Release|Any CPU.Build.0 = Release|Any CPU
{7E666AC9-B717-4E00-A3CB-16950275D3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E666AC9-B717-4E00-A3CB-16950275D3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E666AC9-B717-4E00-A3CB-16950275D3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E666AC9-B717-4E00-A3CB-16950275D3FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
68 changes: 68 additions & 0 deletions TwitchLib.Client/Extensions/SplitExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// proposal: https://github.com/dotnet/runtime/issues/75317
using System;
using System.Runtime.CompilerServices;

namespace TwitchLib.Client.Extensions
{
internal static class SplitExtensions
{
/// <summary>
/// Splits the span into two parts at the first occurrence of a separator.
/// If the separator is not found, Segment will be the entire span and Remainder will be empty.
/// </summary>
/// <typeparam name="T">Span element type</typeparam>
/// <param name="source">Source span to split</param>
/// <param name="separator">Separator value</param>
/// <returns>A split pair of Segment and Remainder, deconstructible with tuple pattern.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ReadOnlySplitPair<T> SplitFirst<T>(this ReadOnlySpan<T> source, T separator)
where T : IEquatable<T>
{
var separatorIndex = source.IndexOf(separator);

return separatorIndex > -1
? new(source.Slice(0, separatorIndex), source.Slice(separatorIndex + 1))
: new(source, default);
}

/// <summary>
/// Splits the span into two parts at the first occurrence of a separator.
/// If the separator is not found, Segment will be the entire span and Remainder will be empty.
/// </summary>
/// <typeparam name="T">Span element type</typeparam>
/// <param name="source">Source span to split</param>
/// <param name="separator">Separator value</param>
/// <returns>A split pair of Segment and Remainder, deconstructible with tuple pattern.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ReadOnlySplitPair<T> SplitLast<T>(this ReadOnlySpan<T> source, T separator)
where T : IEquatable<T>
{
var separatorIndex = source.LastIndexOf(separator);

return separatorIndex > -1
? new(source.Slice(0, separatorIndex), source.Slice(separatorIndex + 1))
: new(source, default);
}

internal readonly ref struct ReadOnlySplitPair<T>
{
public readonly ReadOnlySpan<T> Segment;

public readonly ReadOnlySpan<T> Remainder;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySplitPair(ReadOnlySpan<T> segment, ReadOnlySpan<T> remainder)
{
Segment = segment;
Remainder = remainder;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Deconstruct(out ReadOnlySpan<T> segment, out ReadOnlySpan<T> remainder)
{
segment = Segment;
remainder = Remainder;
}
}
}
}
Loading