diff --git a/.github/workflows/check-buildstatus.yml b/.github/workflows/check-buildstatus.yml index a5c4d329..26693bd5 100644 --- a/.github/workflows/check-buildstatus.yml +++ b/.github/workflows/check-buildstatus.yml @@ -1,4 +1,4 @@ -name: Check TwitchLib.Api PR Build Status +name: Check TwitchLib.Client PR Build Status on: pull_request: @@ -10,12 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - - name: Build TwitchLib.Api + - name: Build TwitchLib.Client run: dotnet build --no-restore + - name: Test TwitchLib.Client + run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 3a7e2db0..3c2a6af8 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -8,21 +8,31 @@ jobs: release-preview: if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')" runs-on: ubuntu-latest + permissions: + id-token: write # enable GitHub OIDC token issuance for this job steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 + - uses: benjlevesque/short-sha@v3.0 + id: short-sha - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build TwitchLib.Client run: dotnet build -c Release --no-restore - name: Pack TwitchLib.Client - run: dotnet pack TwitchLib.Client.sln -v normal -c Release -o nugets --no-build --version-suffix "preview-${{ github.sha }}" + run: dotnet pack -v normal -c Release --no-build --version-suffix "preview.${{ github.run_number }}.${{ steps.short-sha.outputs.sha }}" + # Get a short-lived NuGet API key + - name: NuGet login (OIDC → temp API key) + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} - name: Push to Nuget - run: dotnet nuget push "./nugets/*.nupkg" -k ${{ secrets.API_NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json + run: dotnet nuget push "./artifacts/package/release/*.nupkg" --api-key ${{steps.login.outputs.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json - name: Send Discord Notification uses: sarisia/actions-status-discord@v1 if: always() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 026ef91a..3e1bfbb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,21 +8,29 @@ jobs: release: if: "contains(toJSON(github.event.commits.*.message), '[Release]')" runs-on: ubuntu-latest - + permissions: + id-token: write # enable GitHub OIDC token issuance for this job + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build TwitchLib.Client run: dotnet build -c Release --no-restore - name: Pack TwitchLib.Client - run: dotnet pack TwitchLib.Client.sln -v normal -c Release -o nugets --no-build + run: dotnet pack -v normal -c Release --no-build + # Get a short-lived NuGet API key + - name: NuGet login (OIDC → temp API key) + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} - name: Push to Nuget - run: dotnet nuget push "./nugets/*.nupkg" -k ${{ secrets.API_NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json + run: dotnet nuget push "./artifacts/package/release/*.nupkg" --api-key ${{steps.login.outputs.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json - name: Send Discord Notification uses: sarisia/actions-status-discord@v1 if: always() @@ -30,4 +38,4 @@ jobs: webhook: ${{ secrets.DISCORD_WEBHOOK }} title: "Release Nuget Package" username: "TwitchLib Deploy" - \ No newline at end of file + diff --git a/.gitignore b/.gitignore index 89ad0e0e..c7b1e2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -188,6 +188,9 @@ UpgradeLog*.htm # Microsoft Fakes FakesAssemblies/ +# BenchmarkDotNet artifacts +BenchmarkDotNet.Artifacts/ + # ========================= # Operating System Files # ========================= @@ -233,4 +236,6 @@ $RECYCLE.BIN/ *.msp # Windows shortcuts -*.lnk \ No newline at end of file +*.lnk + +.idea/* \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..44941166 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,36 @@ + + + + + $(MSBuildThisFileDirectory)artifacts + enable + enable + latest + strict + + en-US + True + + + + + swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin + Copyright 2025 + twitchlib.png + https://github.com/TwitchLib/TwitchLib.Client + https://github.com/TwitchLib/TwitchLib.Client + Git + twitch library irc chat c# csharp api events net + a lot of changes + 4.0.1 + $(VersionSuffix) + $(VersionPrefix) + $(VersionPrefix) + MIT + + + + + + + diff --git a/README.md b/README.md index ba9c95a2..8d924345 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,83 @@ -# TwitchLib.Client +# TwitchLib.Client Client component of TwitchLib. -For a general overview and example, refer to https://github.com/TwitchLib/TwitchLib/blob/master/README.md +> [!TIP] +> With the introduction of Chat on EventSub, it is recommended to upgrade your chatbots that are using Twitch IRC to use EventSub (for reading chat messages and roomstates) and Twitch API (for sending chat messages). + +## Installation + +| NuGet | | [![TwitchLib.Client][1]][2] | +| :--------------- | ----: | :---------------------------------------------------------------- | +| Package Manager | `PM>` | `Install-Package TwitchLib.Client -Version 4.0.1` | +| .NET CLI | `>` | `dotnet add package TwitchLib.Client --version 4.0.1` | +| PackageReference | | `` | +| Paket CLI | `>` | `paket add TwitchLib.Client --version 4.0.1` | + +[1]: https://img.shields.io/nuget/v/TwitchLib.Client.svg?label=TwitchLib.Client +[2]: https://www.nuget.org/packages/TwitchLib.Client + +## ⚠ Breaking Changes in Version 4.0.1 ⚠ + +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) 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 +- rewritten all models in `TwitchLib.Client.Models` + - some props/classes can be slightly renamed + - some properties (`IsModerator`, `IsSubscriber`, `HasTurbo`, `IsVip`, `IsPartner`, `IsStaff`) moved to the `UserDetails` property + +## Minimal Setup +Step 1: Create a new Console project + +Step 2: Install the `TwitchLib.Client` NuGet package. (See above on how to do that) + +Step 2.5: (Optional) Install the [`Microsoft.Extensions.Logging.Console`](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console) NuGet package (or some other compatible logging provider) to see logs. + +Step 3: Add the following code to your `Program.cs` file: +```cs +using Microsoft.Extensions.Logging; +using TwitchLib.Client; +using TwitchLib.Client.Events; +using TwitchLib.Client.Models; + +// Optional logger +var loggerFactory = LoggerFactory.Create(c => c + .AddConsole() +// .SetMinimumLevel(LogLevel.Trace) // uncomment to view raw messages received from twitch +); + +var credentials = new ConnectionCredentials(); // anonymous user, add Username and OAuth token to get the ability to send messages +var client = new TwitchClient(loggerFactory: loggerFactory); + +client.Initialize(credentials); +client.OnConnected += Client_OnConnected; +client.OnJoinedChannel += Client_OnJoinedChannel; +client.OnMessageReceived += Client_OnMessageReceived; + +await client.ConnectAsync(); +await Task.Delay(Timeout.Infinite); + + +async Task Client_OnConnected(object? sender, OnConnectedEventArgs e) +{ + await client.JoinChannelAsync("channel_name"); // replace with the channel you want to join +} + +async Task Client_OnJoinedChannel(object? sender, OnJoinedChannelArgs e) +{ + Console.WriteLine($"Connected to {e.Channel}"); + await client.SendMessageAsync(e.Channel, "Hey guys! I am a bot connected via TwitchLib!"); +} + +async Task Client_OnMessageReceived(object? sender, OnMessageReceivedArgs e) +{ + Console.WriteLine($"{e.ChatMessage.Username}#{e.ChatMessage.Channel}: {e.ChatMessage.Message}"); +} +``` +Step 4: Change the `channel_name` in the `Client_OnConnected` method to the name of the channel you want to join and run the application. + +More complete examples can be found in the [TwitchLib.Client.Example](/TwitchLib.Client.Example/) diff --git a/TwitchLib.Client.Benchmark/ChatMessage.cs b/TwitchLib.Client.Benchmark/ChatMessage.cs new file mode 100644 index 00000000..75c562fd --- /dev/null +++ b/TwitchLib.Client.Benchmark/ChatMessage.cs @@ -0,0 +1,22 @@ +using BenchmarkDotNet.Attributes; +using TwitchLib.Client.Parsing; +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Benchmark +{ + [ShortRunJob, MemoryDiagnoser] + public class ChatMessageBenchmark + { + private static readonly IrcMessage Message = IrcParser.ParseMessage( + "@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"); + + private readonly MessageEmoteCollection ChannelEmotes = new(); + + [Benchmark] + public ChatMessage ClientScenarioCreate() + { + return new("test_name", Message, ChannelEmotes); + } + } +} diff --git a/TwitchLib.Client.Benchmark/IrcMessageHandler.cs b/TwitchLib.Client.Benchmark/IrcMessageHandler.cs new file mode 100644 index 00000000..645963cb --- /dev/null +++ b/TwitchLib.Client.Benchmark/IrcMessageHandler.cs @@ -0,0 +1,18 @@ +using BenchmarkDotNet.Attributes; +using TwitchLib.Client.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"); + } + } +} diff --git a/TwitchLib.Client.Benchmark/IrcParser.cs b/TwitchLib.Client.Benchmark/IrcParser.cs new file mode 100644 index 00000000..c093de85 --- /dev/null +++ b/TwitchLib.Client.Benchmark/IrcParser.cs @@ -0,0 +1,23 @@ +using BenchmarkDotNet.Attributes; +using TwitchLib.Client.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!); + } +} diff --git a/TwitchLib.Client.Benchmark/Program.cs b/TwitchLib.Client.Benchmark/Program.cs new file mode 100644 index 00000000..e4ee9667 --- /dev/null +++ b/TwitchLib.Client.Benchmark/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args); \ No newline at end of file diff --git a/TwitchLib.Client.Benchmark/TwitchLib.Client.Benchmark.csproj b/TwitchLib.Client.Benchmark/TwitchLib.Client.Benchmark.csproj new file mode 100644 index 00000000..8987b778 --- /dev/null +++ b/TwitchLib.Client.Benchmark/TwitchLib.Client.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + false + nullable + + + + + + + + + + + diff --git a/TwitchLib.Client.Enums/ChatColorPresets.cs b/TwitchLib.Client.Enums/ChatColorPresets.cs deleted file mode 100644 index 47748db4..00000000 --- a/TwitchLib.Client.Enums/ChatColorPresets.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TwitchLib.Client.Enums -{ - /// Enum representing the available chat name color presets. - public enum ChatColorPresets - { - /// Chat color name. - Blue, - /// Chat color name. - Coral, - /// Chat color name. - DodgerBlue, - /// Chat color name. - SpringGreen, - /// Chat color name. - YellowGreen, - /// Chat color name. - Green, - /// Chat color name. - OrangeRed, - /// Chat color name. - Red, - /// Chat color name. - GoldenRod, - /// Chat color name. - HotPink, - /// Chat color name. - CadetBlue, - /// Chat color name. - SeaGreen, - /// Chat color name. - Chocolate, - /// Chat color name. - BlueViolet, - /// Chat color name. - Firebrick - } -} diff --git a/TwitchLib.Client.Enums/CommercialLength.cs b/TwitchLib.Client.Enums/CommercialLength.cs deleted file mode 100644 index 41a4d378..00000000 --- a/TwitchLib.Client.Enums/CommercialLength.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace TwitchLib.Client.Enums -{ - /// A list of valid commercial lengths. - public enum CommercialLength - { - /// 30 second commercial - Seconds30 = 30, - /// 60 second commercial - Seconds60 = 60, - /// 90 second commercial - Seconds90 = 90, - /// 120 second commercial - Seconds120 = 120, - /// 150 second commercial - Seconds150 = 150, - /// 180 second commercial - Seconds180 = 180 - } -} diff --git a/TwitchLib.Client.Enums/PaidLevel.cs b/TwitchLib.Client.Enums/PaidLevel.cs new file mode 100644 index 00000000..75ceae4b --- /dev/null +++ b/TwitchLib.Client.Enums/PaidLevel.cs @@ -0,0 +1,18 @@ +namespace TwitchLib.Client.Enums; + +/// +/// The level of the Hype Chat. +/// +public enum PaidLevel +{ + One = 1, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, +} diff --git a/TwitchLib.Client.Enums/StringEnum.cs b/TwitchLib.Client.Enums/StringEnum.cs deleted file mode 100644 index b7ed9ba3..00000000 --- a/TwitchLib.Client.Enums/StringEnum.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace TwitchLib.Client.Enums -{ - ///Really cool way of doing string enums, credits to RogueException (https://github.com/RogueException) - public abstract class StringEnum - { - /// Value of enum - public string Value { get; } - - /// StringEnum constructor. - protected StringEnum(string value) - { - Value = value; - } - - /// Returns string value for overriden ToString() - /// Enum value - public override string ToString() => Value; - } -} diff --git a/TwitchLib.Client.Enums/ThrottleType.cs b/TwitchLib.Client.Enums/ThrottleType.cs deleted file mode 100644 index 7fded858..00000000 --- a/TwitchLib.Client.Enums/ThrottleType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TwitchLib.Client.Enums -{ - /// Enum representing the available throttle types. - public enum ThrottleType - { - /// Throttle based on message being too short. - MessageTooShort = 0, - /// Throttle based on message being too long. - MessageTooLong = 1 - } -} diff --git a/TwitchLib.Client.Enums/TwitchLib.Client.Enums.csproj b/TwitchLib.Client.Enums/TwitchLib.Client.Enums.csproj index bcac43e6..b6eb516d 100644 --- a/TwitchLib.Client.Enums/TwitchLib.Client.Enums.csproj +++ b/TwitchLib.Client.Enums/TwitchLib.Client.Enums.csproj @@ -1,24 +1,8 @@  - netstandard2.0 - TwitchLib.Client.Enums - 3.4.0 - $(VersionSuffix) + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 Project containing all of the enums used in TwitchLib.Client. - true - swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin - https://cdn.syzuna-programs.de/images/twitchlib.png - https://github.com/TwitchLib/TwitchLib.Client - https://opensource.org/licenses/MIT - Copyright 2023 - Incremental changes. See commit history. - https://github.com/TwitchLib/TwitchLib.Client - Git - twitch library irc chat c# csharp api events pubsub net standard 2.0 - en-US - 3.4.0 - 3.4.0 - True + diff --git a/TwitchLib.Client.Enums/UserDetails.cs b/TwitchLib.Client.Enums/UserDetails.cs new file mode 100644 index 00000000..f35aaeaa --- /dev/null +++ b/TwitchLib.Client.Enums/UserDetails.cs @@ -0,0 +1,16 @@ +using System; + +namespace TwitchLib.Client.Enums; + +[Flags] +public enum UserDetails +{ + None = 0, + Moderator = 1, + Turbo = 2, + Subscriber = 4, + Vip = 8, + + Partner = 16, + Staff = 32, +} diff --git a/TwitchLib.Client.Example/Program.cs b/TwitchLib.Client.Example/Program.cs new file mode 100644 index 00000000..3ad01537 --- /dev/null +++ b/TwitchLib.Client.Example/Program.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using TwitchLib.Client; +using TwitchLib.Client.Events; +using TwitchLib.Client.Models; + +var loggerFactory = LoggerFactory.Create(c => c + .AddConsole() + //.SetMinimumLevel(LogLevel.Trace) // uncomment to view raw messages received from twitch +); + +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 '!' +}; + +client.Initialize(credentials); +client.OnConnected += Client_OnConnected; +client.OnJoinedChannel += Client_OnJoinedChannel; +client.OnMessageReceived += Client_OnMessageReceived; + +client.OnChatCommandReceived += Client_OnChatCommandReceived; + +await client.ConnectAsync(); +await Task.Delay(Timeout.Infinite); + + +async Task Client_OnConnected(object? sender, OnConnectedEventArgs e) +{ + await client.JoinChannelAsync("channel_name"); // replace with the channel you want to join +} + +async Task Client_OnJoinedChannel(object? sender, OnJoinedChannelArgs e) +{ + Console.WriteLine($"Connected to {e.Channel}"); + await client.SendMessageAsync(e.Channel, "Hey guys! I am a bot connected via TwitchLib!"); +} + +async Task Client_OnMessageReceived(object? sender, OnMessageReceivedArgs e) +{ + Console.WriteLine($"{e.ChatMessage.Username}#{e.ChatMessage.Channel}: {e.ChatMessage.Message}"); +} + +async Task Client_OnChatCommandReceived(object? sender, OnChatCommandReceivedArgs e) +{ + var channel = e.ChatMessage.Channel; + var command = e.Command; + + switch (command.Name.ToLower()) + { + case "info": + await client.SendMessageAsync(channel, "Hi, I am a bot created using TwitchLib.Client."); + break; + case "repeat": + if (string.IsNullOrWhiteSpace(command.ArgumentsAsString)) + return; + await client.SendMessageAsync(channel, command.ArgumentsAsString); + break; + case "leave": + if (!e.ChatMessage.IsBroadcaster) + return; + await client.SendMessageAsync(channel, "Goodbye! I am leaving the channel."); + await client.LeaveChannelAsync(channel); + break; + case "help": + if (command.ArgumentsAsList.Count is 0) + { + var helpText = "Available commands: !info, !repeat , !help [command]"; + if (e.ChatMessage.IsBroadcaster) + helpText += ", !leave"; + + await client.SendMessageAsync(channel, helpText); + return; + } + switch (command.ArgumentsAsList[0]) + { + case "info": + await client.SendMessageAsync(channel, "The !info command provides information about the bot."); + break; + case "repeat": + await client.SendMessageAsync(channel, "The !repeat command repeats the message you provide."); + break; + case "help": + await client.SendMessageAsync(channel, "The !help command provides information about available commands."); + break; + case "leave": + if (!e.ChatMessage.IsBroadcaster) + return; + + await client.SendMessageAsync(channel, "The !leave command makes the bot leave the channel."); + break; + default: + await client.SendMessageAsync(channel, $"No help available for command: {command.ArgumentsAsList[0]}"); + break; + } + break; + default: + Console.WriteLine($"Unknown command: {command.Name}"); + break; + } +} diff --git a/TwitchLib.Client.Example/TwitchLib.Client.Example.csproj b/TwitchLib.Client.Example/TwitchLib.Client.Example.csproj new file mode 100644 index 00000000..4fe86fd6 --- /dev/null +++ b/TwitchLib.Client.Example/TwitchLib.Client.Example.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + false + + + + + + + + + + + diff --git a/TwitchLib.Client.Models/Announcement.cs b/TwitchLib.Client.Models/Announcement.cs index 5fd56c37..856c1b7b 100644 --- a/TwitchLib.Client.Models/Announcement.cs +++ b/TwitchLib.Client.Models/Announcement.cs @@ -1,185 +1,78 @@ -using System.Collections.Generic; -using System.Drawing; -using TwitchLib.Client.Enums; -using TwitchLib.Client.Models.Extensions.NetCore; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models -{ - /// Class representing Announcement in a Twitch channel. - public class Announcement - { - /// Property representing announcement message id - public string Id { get; } - - /// Property representing list of badges assigned. - public List> Badges { get; } - - /// Metadata associated with each badge - public List> BadgeInfo { get; } - - /// Property representing internal system message value. - public string SystemMessage { get; } - - /// Property representing internal system message value, parsed. - public string SystemMessageParsed { get; } - - /// Property representing whether or not the announcer is the broadcaster. - public bool IsBroadcaster { get; } - - /// Property representing whether or not the announcer is a moderator. - public bool IsModerator { get; } - - /// Property representing whether or not announcer is a partner. - public bool IsPartner { get; } - - /// Property representing whether or not the announcer is a subscriber. - public bool IsSubscriber { get; } - - /// Property representing whether or not the announcer is a twitch staff member. - public bool IsStaff { get; } - - /// Property representing whether or not the announcer is a turbo member. - public bool IsTurbo { get; } - - /// Property representing the user's login. - public string Login { get; } - - /// Property representing the user's id. - public string UserId { get; } +namespace TwitchLib.Client.Models; - /// Property representing the room id. - public string RoomId { get; } - - /// Property representing the user type of the announcer. - public UserType UserType { get; } - - /// Property representing the tmi-sent-ts value. - public string TmiSentTs { get; } - - /// Property representing emote set of announcement. - public string EmoteSet { get; } - - /// Property representing the raw IRC message (for debugging/customized parsing) - public string RawIrc { get; } - - /// Property representing the msg-id value. - public string MsgId { get; } - - /// Property representing the color value of the announcement. - public string MsgParamColor { get; } +/// Class representing Announcement in a Twitch channel. +public class Announcement : UserNoticeBase +{ + /// Property representing the color value of the announcement. + public string MsgParamColor { get; protected set; } = default!; - /// Property representing the colorhex of the announcer. - public string ColorHex { get; } + /// Property representing the message of the announcement. + public string Message { get; protected set; } - /// Property representing HEX color as a System.Drawing.Color object. - public Color Color { get; } + /// + /// Initializes a new instance of the class. + /// + /// The IRC message from Twitch to be processed. + public Announcement(IrcMessage ircMessage) : base(ircMessage) + { + Message = ircMessage.Message; + } - /// Property representing the message of the announcement. - public string Message { get; } + /// + /// Initializes a new instance of the class. + /// + public Announcement( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + string msgParamColor, + string message) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamColor = msgParamColor; + Message = message; + } - /// SAMPLE: @badge-info=;badges=broadcaster/1,ambassador/1;color=#033700;display-name=BarryCarlyon;emotes=;flags=;id=55d90904-e515-47d0-ac1d-879f7f1d7b01;login=barrycarlyon;mod=0;msg-id=announcement;msg-param-color=PRIMARY;room-id=15185913;subscriber=0;system-msg=;tmi-sent-ts=1648758023469;user-id=15185913;user-type= :tmi.twitch.tv USERNOTICE #barrycarlyon :test announcment main - /// Announcement object constructor. - /// The IRC message from Twitch to be processed. - public Announcement(IrcMessage ircMessage) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { - RawIrc = ircMessage.ToString(); - Message = ircMessage.Message; - - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - foreach (var badge in Badges) - { - switch (badge.Key) - { - case "broadcaster": - IsBroadcaster = true; - break; - case "turbo": - IsTurbo = true; - break; - case "moderator": - IsModerator = true; - break; - case "subscriber": - IsSubscriber = true; - break; - case "admin": - IsStaff = true; - break; - case "staff": - IsStaff = true; - break; - case "partner": - IsPartner = true; - break; - } - } - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - ColorHex = tagValue; - if (!string.IsNullOrEmpty(ColorHex)) - Color = ColorTranslator.FromHtml(ColorHex); - break; - case Tags.MsgParamColor: - MsgParamColor = tagValue; - break; - case Tags.Emotes: - EmoteSet = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.SystemMsg: - SystemMessage = tagValue; - SystemMessageParsed = tagValue.Replace("\\s", " "); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.UserId: - UserId = tagValue; - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - } - } + case Tags.MsgParamColor: + MsgParamColor = tag.Value; + break; + default: + return false; } + return true; } -} \ No newline at end of file +} diff --git a/TwitchLib.Client.Models/AnonGiftPaidUpgrade.cs b/TwitchLib.Client.Models/AnonGiftPaidUpgrade.cs new file mode 100644 index 00000000..9031ff31 --- /dev/null +++ b/TwitchLib.Client.Models/AnonGiftPaidUpgrade.cs @@ -0,0 +1,82 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class AnonGiftPaidUpgrade : UserNoticeBase +{ + /// + /// The number of gifts the gifter has given during the promo indicated by . + /// + public int MsgParamPromoGiftTotal { get; protected set; } + + /// + /// The subscriptions promo, if any, that is ongoing (for example, Subtember 2018). + /// + public string MsgParamPromoName { get; protected set; } = default!; + + /// + /// Initializes a new instance of the class. + /// + public AnonGiftPaidUpgrade(IrcMessage ircMessage) : base(ircMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public AnonGiftPaidUpgrade( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + int msgParamPromoGiftTotal, + string msgParamPromoName) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamPromoGiftTotal = msgParamPromoGiftTotal; + MsgParamPromoName = msgParamPromoName; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamPromoGiftTotal: + MsgParamPromoGiftTotal = int.Parse(tag.Value); + break; + case Tags.MsgParamPromoName: + MsgParamPromoName = tag.Value; + break; + default: + return false; + } + return true; + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/BitsBadgeTier.cs b/TwitchLib.Client.Models/BitsBadgeTier.cs new file mode 100644 index 00000000..d92d32b1 --- /dev/null +++ b/TwitchLib.Client.Models/BitsBadgeTier.cs @@ -0,0 +1,70 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class BitsBadgeTier : UserNoticeBase +{ + /// + /// The tier of the Bits badge the user just earned. For example, 100, 1000, or 10000. + /// + public int MsgParamThreshold { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public BitsBadgeTier(IrcMessage ircMessage) : base(ircMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public BitsBadgeTier( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + int msgParamThreshold) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamThreshold = msgParamThreshold; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamThreshold: + MsgParamThreshold = int.Parse(tag.Value); + break; + } + return true; + } +} diff --git a/TwitchLib.Client.Models/Builders/ChannelStateBuilder.cs b/TwitchLib.Client.Models/Builders/ChannelStateBuilder.cs deleted file mode 100644 index bd9bb1ce..00000000 --- a/TwitchLib.Client.Models/Builders/ChannelStateBuilder.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ChannelStateBuilder : IBuilder, IFromIrcMessageBuilder - { - private string _broadcasterLanguage; - private string _channel; - private bool _emoteOnly; - private TimeSpan _followersOnly; - private bool _mercury; - private bool _r9K; - private bool _rituals; - private string _roomId; - private int _slowMode; - private bool _subOnly; - - private ChannelStateBuilder() - { - } - - public ChannelStateBuilder WithBroadcasterLanguage(string broadcasterLanguage) - { - _broadcasterLanguage = broadcasterLanguage; - return this; - } - - public ChannelStateBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public ChannelStateBuilder WithEmoteOnly(bool emoteOnly) - { - _emoteOnly = emoteOnly; - return this; - } - - public ChannelStateBuilder WIthFollowersOnly(TimeSpan followersOnly) - { - _followersOnly = followersOnly; - return this; - } - - public ChannelStateBuilder WithMercury(bool mercury) - { - _mercury = mercury; - return this; - } - - public ChannelStateBuilder WithR9K(bool r9k) - { - _r9K = r9k; - return this; - } - - public ChannelStateBuilder WithRituals(bool rituals) - { - _rituals = rituals; - return this; - } - - public ChannelStateBuilder WithRoomId(string roomId) - { - _roomId = roomId; - return this; - } - - public ChannelStateBuilder WithSlowMode(int slowMode) - { - _slowMode = slowMode; - return this; - } - - public ChannelStateBuilder WithSubOnly(bool subOnly) - { - _subOnly = subOnly; - return this; - } - - public static ChannelStateBuilder Create() - { - return new ChannelStateBuilder(); - } - - public ChannelState Build() - { - return new ChannelState( - _r9K, - _rituals, - _subOnly, - _slowMode, - _emoteOnly, - _broadcasterLanguage, - _channel, - _followersOnly, - _mercury, - _roomId); - } - - public ChannelState BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new ChannelState(fromIrcMessageBuilderDataObject.Message); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/ChatCommandBuilder.cs b/TwitchLib.Client.Models/Builders/ChatCommandBuilder.cs deleted file mode 100644 index 74976f0c..00000000 --- a/TwitchLib.Client.Models/Builders/ChatCommandBuilder.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ChatCommandBuilder : IBuilder - { - private readonly List _argumentsAsList = new List(); - private string _argumentsAsString; - private ChatMessage _chatMessage; - private char _commandIdentifier; - private string _commandText; - - private ChatCommandBuilder() - { - } - - public ChatCommandBuilder WithArgumentsAsList(params string[] argumentsList) - { - _argumentsAsList.AddRange(argumentsList); - return this; - } - - public ChatCommandBuilder WithArgumentsAsString(string argumentsAsString) - { - _argumentsAsString = argumentsAsString; - return this; - } - - public ChatCommandBuilder WithChatMessage(ChatMessage chatMessage) - { - _chatMessage = chatMessage; - return this; - } - - public ChatCommandBuilder WithCommandIdentifier(char commandIdentifier) - { - _commandIdentifier = commandIdentifier; - return this; - } - - public ChatCommandBuilder WithCommandText(string commandText) - { - _commandText = commandText; - return this; - } - - public static ChatCommandBuilder Create() - { - return new ChatCommandBuilder(); - } - - public ChatCommand Build() - { - return new ChatCommand( - _chatMessage, - _commandText, - _argumentsAsString, - _argumentsAsList, - _commandIdentifier); - } - - public ChatCommand BuildFromChatMessage(ChatMessage chatMessage) - { - return new ChatCommand(chatMessage); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/ChatMessageBuilder.cs b/TwitchLib.Client.Models/Builders/ChatMessageBuilder.cs deleted file mode 100644 index 3e3dcd2e..00000000 --- a/TwitchLib.Client.Models/Builders/ChatMessageBuilder.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ChatMessageBuilder : IBuilder - { - private TwitchLibMessage _twitchLibMessage; - private readonly List> BadgeInfo = new List>(); - private int _bits; - private double _bitsInDollars; - private string _channel; - private CheerBadge _cheerBadge; - private string _emoteReplacedMessage; - private string _id; - private bool _isBroadcaster; - private bool _isMe; - private bool _isModerator; - private bool _isSubscriber; - private bool _isVip; - private bool _isStaff; - private bool _isPartner; - private string _message; - private Noisy _noisy; - private string _rawIrcMessage; - private string _roomId; - private int _subscribedMonthCount; - - private ChatMessageBuilder() - { - _twitchLibMessage = TwitchLibMessageBuilder.Create().Build(); - } - - public ChatMessageBuilder WithTwitchLibMessage(TwitchLibMessage twitchLibMessage) - { - _twitchLibMessage = twitchLibMessage; - return this; - } - - public ChatMessageBuilder WithBadgeInfos(params KeyValuePair[] badgeInfos) - { - BadgeInfo.AddRange(badgeInfos); - return this; - } - - public ChatMessageBuilder WithBits(int bits) - { - _bits = bits; - return this; - } - - public ChatMessageBuilder WithBitsInDollars(double bitsInDollars) - { - _bitsInDollars = bitsInDollars; - return this; - } - - public ChatMessageBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public ChatMessageBuilder WithCheerBadge(CheerBadge cheerBadge) - { - _cheerBadge = cheerBadge; - return this; - } - - public ChatMessageBuilder WithEmoteReplaceMessage(string emoteReplaceMessage) - { - _emoteReplacedMessage = emoteReplaceMessage; - return this; - } - - public ChatMessageBuilder WithId(string id) - { - _id = id; - return this; - } - - public ChatMessageBuilder WithIsBroadcaster(bool isBroadcaster) - { - _isBroadcaster = isBroadcaster; - return this; - } - - public ChatMessageBuilder WithIsMe(bool isMe) - { - _isMe = isMe; - return this; - } - - public ChatMessageBuilder WithIsModerator(bool isModerator) - { - _isModerator = isModerator; - return this; - } - - public ChatMessageBuilder WithIsSubscriber(bool isSubscriber) - { - _isSubscriber = isSubscriber; - return this; - } - public ChatMessageBuilder WithIsVip(bool isVip) - { - _isVip = isVip; - return this; - } - public ChatMessageBuilder WithIsStaff(bool isStaff) - { - _isStaff = isStaff; - return this; - } - - public ChatMessageBuilder WithIsPartner(bool isPartner) - { - _isPartner = isPartner; - return this; - } - public ChatMessageBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public ChatMessageBuilder WithNoisy(Noisy noisy) - { - _noisy = noisy; - return this; - } - - public ChatMessageBuilder WithRawIrcMessage(string rawIrcMessage) - { - _rawIrcMessage = rawIrcMessage; - return this; - } - - public ChatMessageBuilder WithRoomId(string roomId) - { - _roomId = roomId; - return this; - } - - public ChatMessageBuilder WithSubscribedMonthCount(int subscribedMonthCount) - { - _subscribedMonthCount = subscribedMonthCount; - return this; - } - - public static ChatMessageBuilder Create() - { - return new ChatMessageBuilder(); - } - - public ChatMessage Build() - { - return new ChatMessage( - _twitchLibMessage.BotUsername, - _twitchLibMessage.UserId, - _twitchLibMessage.Username, - _twitchLibMessage.DisplayName, - _twitchLibMessage.ColorHex, - _twitchLibMessage.Color, - _twitchLibMessage.EmoteSet, - _message, - _twitchLibMessage.UserType, - _channel, - _id, - _isSubscriber, - _subscribedMonthCount, - _roomId, - _twitchLibMessage.IsTurbo, - _isModerator, - _isMe, - _isBroadcaster, - _isVip, - _isPartner, - _isStaff, - _noisy, - _rawIrcMessage, - _emoteReplacedMessage, - _twitchLibMessage.Badges, - _cheerBadge, - _bits, - _bitsInDollars); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/CheerBadgeBuilder.cs b/TwitchLib.Client.Models/Builders/CheerBadgeBuilder.cs deleted file mode 100644 index c7b092c8..00000000 --- a/TwitchLib.Client.Models/Builders/CheerBadgeBuilder.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class CheerBadgeBuilder : IBuilder - { - private int _cheerAmount; - - public CheerBadgeBuilder WithCheerAmount(int cheerAmount) - { - _cheerAmount = cheerAmount; - return this; - } - - public CheerBadge Build() - { - return new CheerBadge(_cheerAmount); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/CommunitySubscriptionBuilder.cs b/TwitchLib.Client.Models/Builders/CommunitySubscriptionBuilder.cs deleted file mode 100644 index dff4fbaf..00000000 --- a/TwitchLib.Client.Models/Builders/CommunitySubscriptionBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class CommunitySubscriptionBuilder : IFromIrcMessageBuilder - { - private CommunitySubscriptionBuilder() - { - } - - public static CommunitySubscriptionBuilder Create() - { - return new CommunitySubscriptionBuilder(); - } - - public CommunitySubscription BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new CommunitySubscription(fromIrcMessageBuilderDataObject.Message); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/ConnectionCredentialsBuilder.cs b/TwitchLib.Client.Models/Builders/ConnectionCredentialsBuilder.cs deleted file mode 100644 index 386acb7a..00000000 --- a/TwitchLib.Client.Models/Builders/ConnectionCredentialsBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ConnectionCredentialsBuilder : IBuilder - { - private string _twitchUsername; - private string _twitchOAuth; - private string _twitchWebsocketURI = ConnectionCredentials.DefaultWebSocketUri; - private bool _disableUsernameCheck; - - private ConnectionCredentialsBuilder() - { - } - - public ConnectionCredentialsBuilder WithTwitchUsername(string twitchUsername) - { - _twitchUsername = twitchUsername; - return this; - } - - public ConnectionCredentialsBuilder WithTwitchOAuth(string twitchOAuth) - { - _twitchOAuth = twitchOAuth; - return this; - } - - public ConnectionCredentialsBuilder WithTwitchWebSocketUri(string twitchWebSocketUri) - { - _twitchWebsocketURI = twitchWebSocketUri; - return this; - } - - public ConnectionCredentialsBuilder WithDisableUsernameCheck(bool disableUsernameCheck) - { - _disableUsernameCheck = disableUsernameCheck; - return this; - } - - public static ConnectionCredentialsBuilder Create() - { - return new ConnectionCredentialsBuilder(); - } - - public ConnectionCredentials Build() - { - return new ConnectionCredentials( - _twitchUsername, - _twitchOAuth, - twitchWebsocketURI: _twitchWebsocketURI, - disableUsernameCheck: _disableUsernameCheck); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/EmoteBuilder.cs b/TwitchLib.Client.Models/Builders/EmoteBuilder.cs deleted file mode 100644 index 1830953f..00000000 --- a/TwitchLib.Client.Models/Builders/EmoteBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -using static TwitchLib.Client.Models.EmoteSet; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class EmoteBuilder : IBuilder - { - private string _id; - private string _name; - private int _startIndex; - private int _endIndex; - - private EmoteBuilder() - { - } - - public static EmoteBuilder Create() - { - return new EmoteBuilder(); - } - - public EmoteBuilder WithId(string id) - { - _id = id; - return this; - } - - public EmoteBuilder WithName(string name) - { - _name = name; - return this; - } - - public EmoteBuilder WithStartIndex(int startIndex) - { - _startIndex = startIndex; - return this; - } - - public EmoteBuilder WithEndIndex(int endIndex) - { - _endIndex = endIndex; - return this; - } - - public Emote Build() - { - return new Emote( - _id, - _name, - _startIndex, - _endIndex); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/ErrorEventBuilder.cs b/TwitchLib.Client.Models/Builders/ErrorEventBuilder.cs deleted file mode 100644 index 0749d614..00000000 --- a/TwitchLib.Client.Models/Builders/ErrorEventBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ErrorEventBuilder : IBuilder - { - private string _message; - - private ErrorEventBuilder() - { - } - - public ErrorEventBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public static ErrorEventBuilder Create() - { - return new ErrorEventBuilder(); - } - - public ErrorEvent Build() - { - return new ErrorEvent - { - Message = _message - }; - } - } -} diff --git a/TwitchLib.Client.Models/Builders/FromIrcMessageBuilderDataObject.cs b/TwitchLib.Client.Models/Builders/FromIrcMessageBuilderDataObject.cs deleted file mode 100644 index bfd279f0..00000000 --- a/TwitchLib.Client.Models/Builders/FromIrcMessageBuilderDataObject.cs +++ /dev/null @@ -1,11 +0,0 @@ -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Models.Builders -{ - public class FromIrcMessageBuilderDataObject - { - public IrcMessage Message { get; set; } - - public object AditionalData { get; set; } - } -} diff --git a/TwitchLib.Client.Models/Builders/GiftedSubscriptionBuilder.cs b/TwitchLib.Client.Models/Builders/GiftedSubscriptionBuilder.cs deleted file mode 100644 index 5d1783dc..00000000 --- a/TwitchLib.Client.Models/Builders/GiftedSubscriptionBuilder.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class GiftedSubscriptionBuilder : IBuilder, IFromIrcMessageBuilder - { - private readonly List> _badges = new List>(); - private readonly List> _badgeInfo = new List>(); - private string _color; - private string _displayName; - private string _emotes; - private string _id; - private bool _isModerator; - private bool _isSubscriber; - private bool _isTurbo; - private string _login; - private string _msgId; - private string _msgParamMonths; - private string _msgParamRecipientDisplayName; - private string _msgParamRecipientId; - private string _msgParamRecipientUserName; - private string _msgParamSubPlanName; - private string _msgParamMultiMonthGiftDuration; - private SubscriptionPlan _msgParamSubPlan; - private string _roomId; - private string _systemMsg; - private string _systemMsgParsed; - private string _tmiSentTs; - private string _userId; - private UserType _userType; - - private GiftedSubscriptionBuilder() - { - } - - public GiftedSubscriptionBuilder WithBadges(params KeyValuePair[] badges) - { - _badges.AddRange(badges); - return this; - } - - public GiftedSubscriptionBuilder WithBadgeInfos(params KeyValuePair[] badgeInfos) - { - _badgeInfo.AddRange(badgeInfos); - return this; - } - - public GiftedSubscriptionBuilder WithColor(string color) - { - _color = color; - return this; - } - - public GiftedSubscriptionBuilder WithDisplayName(string displayName) - { - _displayName = displayName; - return this; - } - - public GiftedSubscriptionBuilder WithEmotes(string emotes) - { - _emotes = emotes; - return this; - } - - public GiftedSubscriptionBuilder WithId(string id) - { - _id = id; - return this; - } - - public GiftedSubscriptionBuilder WithIsModerator(bool isModerator) - { - _isModerator = isModerator; - return this; - } - - public GiftedSubscriptionBuilder WithIsSubscriber(bool isSubscriber) - { - _isSubscriber = isSubscriber; - return this; - } - - public GiftedSubscriptionBuilder WithIsTurbo(bool isTurbo) - { - _isTurbo = isTurbo; - return this; - } - - public GiftedSubscriptionBuilder WithLogin(string login) - { - _login = login; - return this; - } - - public GiftedSubscriptionBuilder WithMessageId(string msgId) - { - _msgId = msgId; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamCumulativeMonths(string msgParamMonths) - { - _msgParamMonths = msgParamMonths; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamRecipientDisplayName(string msgParamRecipientDisplayName) - { - _msgParamRecipientDisplayName = msgParamRecipientDisplayName; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamRecipientId(string msgParamRecipientId) - { - _msgParamRecipientId = msgParamRecipientId; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamRecipientUserName(string msgParamRecipientUserName) - { - _msgParamRecipientUserName = msgParamRecipientUserName; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamSubPlanName(string msgParamSubPlanName) - { - _msgParamSubPlanName = msgParamSubPlanName; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamMultiMonthGiftDuration(string msgParamMultiMonthGiftDuration) - { - _msgParamMultiMonthGiftDuration = msgParamMultiMonthGiftDuration; - return this; - } - - public GiftedSubscriptionBuilder WithMsgParamSubPlan(SubscriptionPlan msgParamSubPlan) - { - _msgParamSubPlan = msgParamSubPlan; - return this; - } - - public GiftedSubscriptionBuilder WithRoomId(string roomId) - { - _roomId = roomId; - return this; - } - - public GiftedSubscriptionBuilder WithSystemMsg(string systemMsg) - { - _systemMsg = systemMsg; - return this; - } - - public GiftedSubscriptionBuilder WithSystemMsgParsed(string systemMsgParsed) - { - _systemMsgParsed = systemMsgParsed; - return this; - } - - public GiftedSubscriptionBuilder WithTmiSentTs(string tmiSentTs) - { - _tmiSentTs = tmiSentTs; - return this; - } - - public GiftedSubscriptionBuilder WithUserId(string userId) - { - _userId = userId; - return this; - } - - public GiftedSubscriptionBuilder WithUserType(UserType userType) - { - _userType = userType; - return this; - } - - public static GiftedSubscriptionBuilder Create() - { - return new GiftedSubscriptionBuilder(); - } - - public GiftedSubscription Build() - { - return new GiftedSubscription( - _badges, - _badgeInfo, - _color, - _displayName, - _emotes, - _id, - _login, - _isModerator, - _msgId, - _msgParamMonths, - _msgParamRecipientDisplayName, - _msgParamRecipientId, - _msgParamRecipientUserName, - _msgParamSubPlanName, - _msgParamMultiMonthGiftDuration, - _msgParamSubPlan, - _roomId, - _isSubscriber, - _systemMsg, - _systemMsgParsed, - _tmiSentTs, - _isTurbo, - _userType, - _userId); - } - - public GiftedSubscription BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new GiftedSubscription(fromIrcMessageBuilderDataObject.Message); - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/IBuilder.cs b/TwitchLib.Client.Models/Builders/IBuilder.cs deleted file mode 100644 index a607fbe3..00000000 --- a/TwitchLib.Client.Models/Builders/IBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public interface IBuilder - { - T Build(); - } -} diff --git a/TwitchLib.Client.Models/Builders/IFromIrcMessageBuilder.cs b/TwitchLib.Client.Models/Builders/IFromIrcMessageBuilder.cs deleted file mode 100644 index 8269981b..00000000 --- a/TwitchLib.Client.Models/Builders/IFromIrcMessageBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public interface IFromIrcMessageBuilder - { - T BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject); - } -} diff --git a/TwitchLib.Client.Models/Builders/IrcMessageBuilder.cs b/TwitchLib.Client.Models/Builders/IrcMessageBuilder.cs deleted file mode 100644 index 33a76d56..00000000 --- a/TwitchLib.Client.Models/Builders/IrcMessageBuilder.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums.Internal; -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class IrcMessageBuilder : IBuilder - { - private IrcCommand _ircCommand; - private readonly List _parameters = new List(); - private string _hostmask; - private Dictionary _tags; - - public static IrcMessageBuilder Create() - { - return new IrcMessageBuilder(); - } - - private IrcMessageBuilder() - { - } - - public IrcMessageBuilder WithCommand(IrcCommand ircCommand) - { - _ircCommand = ircCommand; - return Builder(); - } - - public IrcMessageBuilder WithParameter(params string[] parameters) - { - _parameters.AddRange(parameters); - return Builder(); - } - - private IrcMessageBuilder Builder() - { - return this; - } - - public IrcMessage BuildWithUserOnly(string user) - { - return new IrcMessage(user); - } - - public IrcMessageBuilder WithHostMask(string hostMask) - { - _hostmask = hostMask; - return Builder(); - } - - public IrcMessageBuilder WithTags(Dictionary tags) - { - _tags = tags; - return Builder(); - } - - public IrcMessage Build() - { - return new IrcMessage(_ircCommand, _parameters.ToArray(), _hostmask, _tags); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/OutboundChatMessageBuilder.cs b/TwitchLib.Client.Models/Builders/OutboundChatMessageBuilder.cs deleted file mode 100644 index 89f7584b..00000000 --- a/TwitchLib.Client.Models/Builders/OutboundChatMessageBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class OutboundChatMessageBuilder : IBuilder - { - private string _channel; - private string _message; - private string _userName; - - private OutboundChatMessageBuilder() - { - } - - public static OutboundChatMessageBuilder Create() - { - return new OutboundChatMessageBuilder(); - } - - public OutboundChatMessageBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public OutboundChatMessageBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public OutboundChatMessageBuilder WithUsername(string userName) - { - _userName = userName; - return this; - } - - public OutboundChatMessage Build() - { - return new OutboundChatMessage - { - Channel = _channel, - Message = _message, - Username = _userName, - }; - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/OutboundWhisperMessageBuilder.cs b/TwitchLib.Client.Models/Builders/OutboundWhisperMessageBuilder.cs deleted file mode 100644 index 5f6827b2..00000000 --- a/TwitchLib.Client.Models/Builders/OutboundWhisperMessageBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class OutboundWhisperMessageBuilder : IBuilder - { - private string _username; - private string _receiver; - private string _message; - - private OutboundWhisperMessageBuilder() - { - } - - public OutboundWhisperMessageBuilder WithUsername(string username) - { - _username = username; - return this; - } - - public OutboundWhisperMessageBuilder WithReceiver(string receiver) - { - _receiver = receiver; - return this; - } - - public OutboundWhisperMessageBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public static OutboundWhisperMessageBuilder Create() - { - return new OutboundWhisperMessageBuilder(); - } - - public OutboundWhisperMessage Build() - { - return new OutboundWhisperMessage - { - Message = _message, - Receiver = _receiver, - Username = _username - }; - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/OutgoingMessageBuilder.cs b/TwitchLib.Client.Models/Builders/OutgoingMessageBuilder.cs deleted file mode 100644 index 7e49b549..00000000 --- a/TwitchLib.Client.Models/Builders/OutgoingMessageBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class OutgoingMessageBuilder : IBuilder - { - private string _channel; - private string _message; - private int _nonce; - private string _sender; - private MessageState _messageState; - - private OutgoingMessageBuilder() - { - } - - public static OutgoingMessageBuilder Create() - { - return new OutgoingMessageBuilder(); - } - - public OutgoingMessageBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public OutgoingMessageBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public OutgoingMessageBuilder WithNonce(int nonce) - { - _nonce = nonce; - return this; - } - - public OutgoingMessageBuilder WithSender(string sender) - { - _sender = sender; - return this; - } - - public OutgoingMessageBuilder WithMessageState(MessageState messageState) - { - _messageState = messageState; - return this; - } - - public OutgoingMessage Build() - { - return new OutgoingMessage - { - Channel = _channel, - Message = _message, - Nonce = _nonce, - Sender = _sender, - State = _messageState - }; - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/RaidNotificationBuilder.cs b/TwitchLib.Client.Models/Builders/RaidNotificationBuilder.cs deleted file mode 100644 index 71abc7b9..00000000 --- a/TwitchLib.Client.Models/Builders/RaidNotificationBuilder.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class RaidNotificationBuilder : IBuilder, IFromIrcMessageBuilder - { - private readonly List> _badges = new List>(); - private readonly List> _badgeInfo = new List>(); - private string _color; - private string _displayName; - private string _emotes; - private string _id; - private bool _isModerator; - private bool _isSubscriber; - private bool _isTurbo; - private string _login; - private string _msgId; - private string _msgParamDisplayName; - private string _msgParamLogin; - private string _msgParamViewerCount; - private string _roomId; - private string _systemMsg; - private string _systemMsgParsed; - private string _tmiSentTs; - private string _userId; - private UserType _userType; - - public RaidNotificationBuilder WithBadges(params KeyValuePair[] badges) - { - _badges.AddRange(badges); - return this; - } - - public RaidNotificationBuilder WithBadgeInfos(params KeyValuePair[] badgeInfos) - { - _badgeInfo.AddRange(badgeInfos); - return this; - } - - public RaidNotificationBuilder WithColor(string color) - { - _color = color; - return this; - } - - public RaidNotificationBuilder WithDisplayName(string displayName) - { - _displayName = displayName; - return this; - } - - public RaidNotificationBuilder WithEmotes(string emotes) - { - _emotes = emotes; - return this; - } - - public RaidNotificationBuilder WithId(string id) - { - _id = id; - return this; - } - - public RaidNotificationBuilder WithIsModerator(bool isModerator) - { - _isModerator = isModerator; - return this; - } - - public RaidNotificationBuilder WithIsSubscriber(bool isSubscriber) - { - _isSubscriber = isSubscriber; - return this; - } - - public RaidNotificationBuilder WithIsTurbo(bool isTurbo) - { - _isTurbo = isTurbo; - return this; - } - - public RaidNotificationBuilder WithLogin(string login) - { - _login = login; - return this; - } - - public RaidNotificationBuilder WithMessageId(string msgId) - { - _msgId = msgId; - return this; - } - - public RaidNotificationBuilder WithMsgParamDisplayName(string msgParamDisplayName) - { - _msgParamDisplayName = msgParamDisplayName; - return this; - } - - public RaidNotificationBuilder WithMsgParamLogin(string msgParamLogin) - { - _msgParamLogin = msgParamLogin; - return this; - } - - public RaidNotificationBuilder WithMsgParamViewerCount(string msgParamViewerCount) - { - _msgParamViewerCount = msgParamViewerCount; - return this; - } - - public RaidNotificationBuilder WithRoomId(string roomId) - { - _roomId = roomId; - return this; - } - - public RaidNotificationBuilder WithSystemMsg(string systemMsg) - { - _systemMsg = systemMsg; - return this; - } - - public RaidNotificationBuilder WithSystemMsgParsed(string systemMsgParsed) - { - _systemMsgParsed = systemMsgParsed; - return this; - } - - public RaidNotificationBuilder WithTmiSentTs(string tmiSentTs) - { - _tmiSentTs = tmiSentTs; - return this; - } - - public RaidNotificationBuilder WithUserId(string userId) - { - _userId = userId; - return this; - } - - public RaidNotificationBuilder WithUserType(UserType userType) - { - _userType = userType; - return this; - } - - private RaidNotificationBuilder() - { - } - - public RaidNotification Build() - { - return new RaidNotification( - _badges, - _badgeInfo, - _color, - _displayName, - _emotes, - _id, - _login, - _isModerator, - _msgId, - _msgParamDisplayName, - _msgParamLogin, - _msgParamViewerCount, - _roomId, - _isSubscriber, - _systemMsg, - _systemMsgParsed, - _tmiSentTs, - _isTurbo, - _userType, - _userId); - } - - public RaidNotification BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new RaidNotification(fromIrcMessageBuilderDataObject.Message); - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/ReSubscriberBuilder.cs b/TwitchLib.Client.Models/Builders/ReSubscriberBuilder.cs deleted file mode 100644 index 76ac1d63..00000000 --- a/TwitchLib.Client.Models/Builders/ReSubscriberBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class ReSubscriberBuilder : SubscriberBaseBuilder, IBuilder, IFromIrcMessageBuilder - { - private ReSubscriberBuilder() - { - } - - public static new ReSubscriberBuilder Create() - { - return new ReSubscriberBuilder(); - } - - public ReSubscriber BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new ReSubscriber(fromIrcMessageBuilderDataObject.Message); - } - - ReSubscriber IBuilder.Build() - { - return (ReSubscriber)Build(); - } - - public override SubscriberBase Build() - { - return new ReSubscriber( - Badges, - BadgeInfo, - ColorHex, - Color, - DisplayName, - EmoteSet, - Id, - Login, - SystemMessage, - MessageId, - MsgParamCumulativeMonths, - MsgParamStreakMonths, - MsgParamShouldShareStreak, - ParsedSystemMessage, - ResubMessage, - SubscriptionPlan, - SubscriptionPlanName, - RoomId, - UserId, - IsModerator, - IsTurbo, - IsSubscriber, - IsPartner, - TmiSentTs, - UserType, - RawIrc, - Channel, - Months); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/RitualNewChatterBuilder.cs b/TwitchLib.Client.Models/Builders/RitualNewChatterBuilder.cs deleted file mode 100644 index af84a44b..00000000 --- a/TwitchLib.Client.Models/Builders/RitualNewChatterBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class RitualNewChatterBuilder : IFromIrcMessageBuilder - { - public RitualNewChatter BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new RitualNewChatter(fromIrcMessageBuilderDataObject.Message); - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Builders/SentMessageBuilder.cs b/TwitchLib.Client.Models/Builders/SentMessageBuilder.cs deleted file mode 100644 index f4672521..00000000 --- a/TwitchLib.Client.Models/Builders/SentMessageBuilder.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class SentMessageBuilder : IBuilder - { - private readonly List> _badges = new List>(); - private string _channel; - private string _colorHex; - private string _displayName; - private string _emoteSet; - private bool _isModerator; - private bool _isSubscriber; - private string _message; - private UserType _userType; - - private SentMessageBuilder() - { - } - - public SentMessageBuilder WithBadges(params KeyValuePair[] badges) - { - _badges.AddRange(badges); - return this; - } - - public SentMessageBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public SentMessageBuilder WithColorHex(string colorHex) - { - _colorHex = colorHex; - return this; - } - - public SentMessageBuilder WithDisplayName(string displayName) - { - _displayName = displayName; - return this; - } - - public SentMessageBuilder WithEmoteSet(string emoteSet) - { - _emoteSet = emoteSet; - return this; - } - - public SentMessageBuilder WithIsModerator(bool isModerator) - { - _isModerator = isModerator; - return this; - } - - public SentMessageBuilder WithIsSubscriber(bool isSubscriber) - { - _isSubscriber = isSubscriber; - return this; - } - - public SentMessageBuilder WithMessage(string message) - { - _message = message; - return this; - } - - public SentMessageBuilder WithUserType(UserType userType) - { - _userType = userType; - return this; - } - - public static SentMessageBuilder Create() - { - return new SentMessageBuilder(); - } - - public SentMessage BuildFromUserState(UserState userState, string message) - { - return new SentMessage(userState, message); - } - - public SentMessage Build() - { - return new SentMessage( - _badges, - _channel, - _colorHex, - _displayName, - _emoteSet, - _isModerator, - _isSubscriber, - _userType, - _message); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/SubscriberBaseBuilder.cs b/TwitchLib.Client.Models/Builders/SubscriberBaseBuilder.cs deleted file mode 100644 index 474efdcc..00000000 --- a/TwitchLib.Client.Models/Builders/SubscriberBaseBuilder.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public class SubscriberBaseBuilder : IBuilder - { - protected List> Badges { get; } = new List>(); - - public List> BadgeInfo { get; } = new List>(); - - protected string ColorHex { get; set; } - - protected Color Color { get; set; } - - protected string DisplayName { get; set; } - - protected string EmoteSet { get; set; } - - protected string Id { get; set; } - - protected bool IsModerator { get; set; } - - protected bool IsPartner { get; set; } - - protected bool IsSubscriber { get; set; } - - protected bool IsTurbo { get; set; } - - protected string Login { get; set; } - - protected string RawIrc { get; set; } - - protected string ResubMessage { get; set; } - - protected string RoomId { get; set; } - - protected SubscriptionPlan SubscriptionPlan { get; set; } - - protected string SubscriptionPlanName { get; set; } - - protected string SystemMessage { get; set; } - - protected string ParsedSystemMessage { get; set; } - - protected string TmiSentTs { get; set; } - - protected string UserId { get; set; } - - protected UserType UserType { get; set; } - - protected string Channel { get; set; } - - protected string MessageId { get; set; } - - protected string MsgParamCumulativeMonths { get; set; } - - protected string MsgParamStreakMonths { get; set; } - - protected bool MsgParamShouldShareStreak { get; set; } - - protected int Months { get; set; } - - protected SubscriberBaseBuilder() - { - } - - public static SubscriberBaseBuilder Create() - { - return new SubscriberBaseBuilder(); - } - - public SubscriberBaseBuilder WithBadges(params KeyValuePair[] badges) - { - Badges.AddRange(badges); - return this; - } - - public SubscriberBaseBuilder WithBadgeInfos(params KeyValuePair[] badgeInfos) - { - BadgeInfo.AddRange(badgeInfos); - return this; - } - - public SubscriberBaseBuilder WithColorHex(string colorHex) - { - ColorHex = colorHex; - return this; - } - - public SubscriberBaseBuilder WithColor(Color color) - { - Color = color; - return this; - } - - public SubscriberBaseBuilder WithDisplayName(string displayName) - { - DisplayName = displayName; - return this; - } - - public SubscriberBaseBuilder WithEmoteSet(string emoteSet) - { - EmoteSet = emoteSet; - return this; - } - - public SubscriberBaseBuilder WithId(string id) - { - Id = id; - return this; - } - - public SubscriberBaseBuilder WithIsModerator(bool isModerator) - { - IsModerator = isModerator; - return this; - } - - public SubscriberBaseBuilder WithIsPartner(bool isPartner) - { - IsPartner = isPartner; - return this; - } - - public SubscriberBaseBuilder WithIsSubscriber(bool isSubscriber) - { - IsSubscriber = isSubscriber; - return this; - } - - public SubscriberBaseBuilder WithIsTurbo(bool isTurbo) - { - IsTurbo = isTurbo; - return this; - } - - public SubscriberBaseBuilder WithLogin(string login) - { - Login = login; - return this; - } - - public SubscriberBaseBuilder WithRawIrc(string rawIrc) - { - RawIrc = rawIrc; - return this; - } - - public SubscriberBaseBuilder WithResubMessage(string resubMessage) - { - ResubMessage = resubMessage; - return this; - } - - public SubscriberBaseBuilder WithRoomId(string roomId) - { - RoomId = roomId; - return this; - } - - public SubscriberBaseBuilder WithSubscribtionPlan(SubscriptionPlan subscriptionPlan) - { - SubscriptionPlan = subscriptionPlan; - return this; - } - - public SubscriberBaseBuilder WithSubscriptionPlanName(string subscriptionPlanName) - { - SubscriptionPlanName = subscriptionPlanName; - return this; - } - - public SubscriberBaseBuilder WithSystemMessage(string systemMessage) - { - SystemMessage = systemMessage; - return this; - } - - public SubscriberBaseBuilder WithParsedSystemMessage(string parsedSystemMessage) - { - ParsedSystemMessage = parsedSystemMessage; - return this; - } - - public SubscriberBaseBuilder WithTmiSentTs(string tmiSentTs) - { - TmiSentTs = tmiSentTs; - return this; - } - - public SubscriberBaseBuilder WithUserType(UserType userType) - { - UserType = userType; - return this; - } - - public SubscriberBaseBuilder WithUserId(string userId) - { - UserId = userId; - return this; - } - - public SubscriberBaseBuilder WithMonths(int months) - { - Months = months; - return this; - } - - public SubscriberBaseBuilder WithMessageId(string messageId) - { - MessageId = messageId; - return this; - } - - public SubscriberBaseBuilder WithMsgParamCumulativeMonths(string msgParamCumulativeMonths) - { - MsgParamCumulativeMonths = msgParamCumulativeMonths; - return this; - } - - public SubscriberBaseBuilder WithMsgParamStreakMonths(string msgParamStreakMonths) - { - MsgParamStreakMonths = msgParamStreakMonths; - return this; - } - - public SubscriberBaseBuilder WithMsgParamShouldShareStreak(bool msgParamShouldShareStreak) - { - MsgParamShouldShareStreak = msgParamShouldShareStreak; - return this; - } - - public SubscriberBaseBuilder WithChannel(string channel) - { - Channel = channel; - return this; - } - - public virtual SubscriberBase Build() - { - return new SubscriberBase( - Badges, - BadgeInfo, - ColorHex, - Color, - DisplayName, - EmoteSet, - Id, - Login, - SystemMessage, - MessageId, - MsgParamCumulativeMonths, - MsgParamStreakMonths, - MsgParamShouldShareStreak, - ParsedSystemMessage, - ResubMessage, - SubscriptionPlan, - SubscriptionPlanName, - RoomId, - UserId, - IsModerator, - IsTurbo, - IsSubscriber, - IsPartner, - TmiSentTs, - UserType, - RawIrc, - Channel, - Months); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/SubscriberBuilder.cs b/TwitchLib.Client.Models/Builders/SubscriberBuilder.cs deleted file mode 100644 index 018daf55..00000000 --- a/TwitchLib.Client.Models/Builders/SubscriberBuilder.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class SubscriberBuilder : SubscriberBaseBuilder, IBuilder, IFromIrcMessageBuilder - { - private SubscriberBuilder() - { - } - - public static new SubscriberBuilder Create() - { - return new SubscriberBuilder(); - } - - public Subscriber BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new Subscriber(fromIrcMessageBuilderDataObject.Message); - } - - Subscriber IBuilder.Build() - { - return (Subscriber)Build(); - } - - public override SubscriberBase Build() - { - return new Subscriber( - Badges, - BadgeInfo, - ColorHex, - Color, - DisplayName, - EmoteSet, - Id, - Login, - SystemMessage, - MessageId, - MsgParamCumulativeMonths, - MsgParamStreakMonths, - MsgParamShouldShareStreak, - ParsedSystemMessage, - ResubMessage, - SubscriptionPlan, - SubscriptionPlanName, - RoomId, - UserId, - IsModerator, - IsTurbo, - IsSubscriber, - IsPartner, - TmiSentTs, - UserType, - RawIrc, - Channel); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/TwitchLibMessageBuilder.cs b/TwitchLib.Client.Models/Builders/TwitchLibMessageBuilder.cs deleted file mode 100644 index b0828213..00000000 --- a/TwitchLib.Client.Models/Builders/TwitchLibMessageBuilder.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class TwitchLibMessageBuilder : TwitchLibMessage, IBuilder - { - private TwitchLibMessageBuilder() - { - } - - public TwitchLibMessageBuilder WithBadges(List> badges) - { - Badges = badges; - return this; - } - - public TwitchLibMessageBuilder WithColorHex(string colorHex) - { - ColorHex = colorHex; - return this; - } - - public TwitchLibMessageBuilder WithColorHex(Color color) - { - Color = color; - return this; - } - - public TwitchLibMessageBuilder WithUsername(string username) - { - Username = username; - return this; - } - - public TwitchLibMessageBuilder WithDisplayName(string displayName) - { - DisplayName = displayName; - return this; - } - - public TwitchLibMessageBuilder WithEmoteSet(EmoteSet emoteSet) - { - EmoteSet = emoteSet; - return this; - } - - public TwitchLibMessageBuilder WithUserId(string userId) - { - UserId = userId; - return this; - } - - public TwitchLibMessageBuilder WithIsTurbo(bool isTurbo) - { - IsTurbo = isTurbo; - return this; - } - - public TwitchLibMessageBuilder WithBotUserName(string botUserName) - { - BotUsername = botUserName; - return this; - } - - public TwitchLibMessageBuilder WithUserType(UserType userType) - { - UserType = userType; - return this; - } - - public static TwitchLibMessageBuilder Create() - { - return new TwitchLibMessageBuilder(); - } - - public TwitchLibMessage Build() - { - return this; - } - } -} diff --git a/TwitchLib.Client.Models/Builders/UserBanBuilder.cs b/TwitchLib.Client.Models/Builders/UserBanBuilder.cs deleted file mode 100644 index c694433f..00000000 --- a/TwitchLib.Client.Models/Builders/UserBanBuilder.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class UserBanBuilder : IBuilder, IFromIrcMessageBuilder - { - private string _channelName; - private string _userName; - private string _banReason; - private string _roomId; - private string _targetUserId; - - public static UserBanBuilder Create() - { - return new UserBanBuilder(); - } - - private UserBanBuilder() - { - } - - public UserBanBuilder WithChannel(string channel) - { - _channelName = channel; - return this; - } - - public UserBanBuilder WithUserName(string userName) - { - _userName = userName; - return this; - } - - public UserBanBuilder WithBanReason(string banReason) - { - _banReason = banReason; - return this; - } - - public UserBanBuilder WithRoomId(string roomId) - { - _roomId = roomId; - return this; - } - - public UserBanBuilder WithTargetUserId(string targetUserId) - { - _targetUserId = targetUserId; - return this; - } - - public UserBan BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new UserBan(fromIrcMessageBuilderDataObject.Message); - } - - public UserBan Build() - { - return new UserBan(_channelName, _userName, _banReason, _roomId, _targetUserId); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/UserStateBuilder.cs b/TwitchLib.Client.Models/Builders/UserStateBuilder.cs deleted file mode 100644 index 6c25a746..00000000 --- a/TwitchLib.Client.Models/Builders/UserStateBuilder.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class UserStateBuilder : IBuilder, IFromIrcMessageBuilder - { - private readonly List> _badges = new List>(); - private readonly List> _badgeInfo = new List>(); - private string _channel; - private string _colorHex; - private string _displayName; - private string _emoteSet; - private string _id; - private bool _isModerator; - private bool _isSubscriber; - private UserType _userType; - - private UserStateBuilder() - { - } - - public UserStateBuilder WithBadges(params KeyValuePair[] badges) - { - _badges.AddRange(badges); - return this; - } - - public UserStateBuilder WithBadgeInfos(params KeyValuePair[] badgeInfos) - { - _badgeInfo.AddRange(badgeInfos); - return this; - } - - public UserStateBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public UserStateBuilder WithColorHex(string olorHex) - { - _colorHex = olorHex; - return this; - } - - public UserStateBuilder WithDisplayName(string displayName) - { - _displayName = displayName; - return this; - } - - public UserStateBuilder WithEmoteSet(string emoteSet) - { - _emoteSet = emoteSet; - return this; - } - - public UserStateBuilder Id(string id) - { - _id = id; - return this; - } - - public UserStateBuilder WithIsModerator(bool isModerator) - { - _isModerator = isModerator; - return this; - } - - public UserStateBuilder WithIsSubscriber(bool isSubscriber) - { - _isSubscriber = isSubscriber; - return this; - } - - public UserStateBuilder WithUserType(UserType userType) - { - _userType = userType; - return this; - } - - public static UserStateBuilder Create() - { - return new UserStateBuilder(); - } - - public UserState BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new UserState(fromIrcMessageBuilderDataObject.Message); - } - - public UserState Build() - { - return new UserState( - _badges, - _badgeInfo, - _colorHex, - _displayName, - _emoteSet, - _channel, - _id, - _isSubscriber, - _isModerator, - _userType); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/UserTimeoutBuilder.cs b/TwitchLib.Client.Models/Builders/UserTimeoutBuilder.cs deleted file mode 100644 index a7998fb5..00000000 --- a/TwitchLib.Client.Models/Builders/UserTimeoutBuilder.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class UserTimeoutBuilder : IBuilder, IFromIrcMessageBuilder - { - private string _channel; - private int _timeoutDuration; - private string _timeoutReason; - private string _username; - private string _targetUserId; - - private UserTimeoutBuilder() - { - } - - public UserTimeoutBuilder WithChannel(string channel) - { - _channel = channel; - return this; - } - - public UserTimeoutBuilder WithTimeoutDuration(int timeoutDuration) - { - _timeoutDuration = timeoutDuration; - return this; - } - - public UserTimeoutBuilder WithTimeoutReason(string timeoutReason) - { - _timeoutReason = timeoutReason; - return this; - } - - public UserTimeoutBuilder WithUsername(string username) - { - _username = username; - return this; - } - public UserTimeoutBuilder WithTargetUserId(string targetUserId) - { - _targetUserId = targetUserId; - return this; - } - - public static UserTimeoutBuilder Create() - { - return new UserTimeoutBuilder(); - } - - public UserTimeout BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - return new UserTimeout(fromIrcMessageBuilderDataObject.Message); - } - - public UserTimeout Build() - { - return new UserTimeout(_channel, _username, _targetUserId, _timeoutDuration, _timeoutReason); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/WhisperCommandBuilder.cs b/TwitchLib.Client.Models/Builders/WhisperCommandBuilder.cs deleted file mode 100644 index 1ec4b44b..00000000 --- a/TwitchLib.Client.Models/Builders/WhisperCommandBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; - -namespace TwitchLib.Client.Models.Builders -{ - public sealed class WhisperCommandBuilder : IBuilder - { - private readonly List _argumentsAsList = new List(); - private string _argumentsAsString; - private char _commandIdentifier; - private string _commandText; - private WhisperMessage _whisperMessage; - - private WhisperCommandBuilder() - { - } - - public WhisperCommandBuilder WithWhisperMessage(WhisperMessage whisperMessage) - { - _whisperMessage = whisperMessage; - return this; - } - - public WhisperCommandBuilder WithCommandText(string commandText) - { - _commandText = commandText; - return this; - } - - public WhisperCommandBuilder WithCommandIdentifier(char commandIdentifier) - { - _commandIdentifier = commandIdentifier; - return this; - } - - public WhisperCommandBuilder WithArgumentAsString(string argumentAsString) - { - _argumentsAsString = argumentAsString; - return this; - } - - public WhisperCommandBuilder WithArguments(params string[] arguments) - { - _argumentsAsList.AddRange(arguments); - return this; - } - - public static WhisperCommandBuilder Create() - { - return new WhisperCommandBuilder(); - } - - public WhisperCommand BuildFromWhisperMessage(WhisperMessage whisperMessage) - { - return new WhisperCommand(whisperMessage); - } - - public WhisperCommand Build() - { - return new WhisperCommand(_whisperMessage, _commandText, _argumentsAsString, _argumentsAsList, _commandIdentifier); - } - } -} diff --git a/TwitchLib.Client.Models/Builders/WhisperMessageBuilder.cs b/TwitchLib.Client.Models/Builders/WhisperMessageBuilder.cs deleted file mode 100644 index f7de11c8..00000000 --- a/TwitchLib.Client.Models/Builders/WhisperMessageBuilder.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace TwitchLib.Client.Models.Builders -{ - public sealed class WhisperMessageBuilder : IBuilder, IFromIrcMessageBuilder - { - private TwitchLibMessage _twitchLibMessage; - private string _messageId; - private string _threadId; - private string _message; - - private WhisperMessageBuilder() - { - _twitchLibMessage = TwitchLibMessageBuilder.Create().Build(); - } - - public static WhisperMessageBuilder Create() - { - return new WhisperMessageBuilder(); - } - - public WhisperMessageBuilder WithTwitchLibMessage(TwitchLibMessage twitchLibMessage) - { - _twitchLibMessage = twitchLibMessage; - return this; - } - - public WhisperMessageBuilder WithMessageId(string messageId) - { - _messageId = messageId; - return this; - } - - public WhisperMessageBuilder WithThreadId(string threadId) - { - _threadId = threadId; - return this; - } - - public WhisperMessageBuilder WhihMessage(string message) - { - _message = message; - return this; - } - - public WhisperMessage BuildFromIrcMessage(FromIrcMessageBuilderDataObject fromIrcMessageBuilderDataObject) - { - string botName = fromIrcMessageBuilderDataObject.AditionalData.ToString(); - return new WhisperMessage(fromIrcMessageBuilderDataObject.Message, botName); - } - - public WhisperMessage Build() - { - return new WhisperMessage( - _twitchLibMessage.Badges, - _twitchLibMessage.ColorHex, - _twitchLibMessage.Color, - _twitchLibMessage.Username, - _twitchLibMessage.DisplayName, - _twitchLibMessage.EmoteSet, - _threadId, - _messageId, - _twitchLibMessage.UserId, - _twitchLibMessage.IsTurbo, - _twitchLibMessage.BotUsername, - _message, - _twitchLibMessage.UserType); - } - } -} diff --git a/TwitchLib.Client.Models/ChannelState.cs b/TwitchLib.Client.Models/ChannelState.cs index 128b95d7..7c8cf616 100644 --- a/TwitchLib.Client.Models/ChannelState.cs +++ b/TwitchLib.Client.Models/ChannelState.cs @@ -1,6 +1,4 @@ -using System; - -using TwitchLib.Client.Models.Internal; +using TwitchLib.Client.Models.Internal; namespace TwitchLib.Client.Models { @@ -8,7 +6,7 @@ namespace TwitchLib.Client.Models public class ChannelState { /// Property representing the current broadcaster language. - public string BroadcasterLanguage { get; } + public string BroadcasterLanguage { get; } = default!; /// Property representing the current channel. public string Channel { get; } @@ -16,7 +14,7 @@ public class ChannelState /// Property representing whether EmoteOnly mode is being applied to chat or not. WILL BE NULL IF VALUE NOT PRESENT. public bool? EmoteOnly { get; } - /// Property representing how long needed to be following to talk. If null, FollowersOnly is not enabled. + /// Property representing how long needed to be following to talk. Timeout.InfiniteTimespan indicates that FollowersOnly mode is switched off. If null, FollowersOnly status is not changed. public TimeSpan? FollowersOnly { get; } = null; /// Property representing mercury value. Not sure what it's for. @@ -29,7 +27,7 @@ public class ChannelState public bool? Rituals { get; } /// Twitch assigned room id - public string RoomId { get; } + public string RoomId { get; } = default!; /// Property representing whether Slow mode is being applied to chat or not. WILL BE NULL IF VALUE NOT PRESENT. public int? SlowMode { get; } @@ -37,55 +35,62 @@ public class ChannelState /// Property representing whether Sub Mode is being applied to chat or not. WILL BE NULL IF VALUE NOT PRESENT. public bool? SubOnly { get; } + /// + /// Contains undocumented tags. + /// + public Dictionary? UndocumentedTags { get; } + /// ChannelState object constructor. public ChannelState(IrcMessage ircMessage) { //@broadcaster-lang=;emote-only=0;r9k=0;slow=0;subs-only=1 :tmi.twitch.tv ROOMSTATE #burkeblack - foreach (var tag in ircMessage.Tags.Keys) + foreach (var tag in ircMessage.Tags) { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) + var tagValue = tag.Value; + switch (tag.Key) { case Tags.BroadcasterLang: BroadcasterLanguage = tagValue; break; case Tags.EmoteOnly: - EmoteOnly = Common.Helpers.ConvertToBool(tagValue); + EmoteOnly = TagHelper.ToBool(tagValue); break; case Tags.R9K: - R9K = Common.Helpers.ConvertToBool(tagValue); + R9K = TagHelper.ToBool(tagValue); break; case Tags.Rituals: - Rituals = Common.Helpers.ConvertToBool(tagValue); + Rituals = TagHelper.ToBool(tagValue); break; case Tags.Slow: - var success = int.TryParse(tagValue, out var slowDuration); + var success = int.TryParse(tag.Value, out var slowDuration); SlowMode = success ? slowDuration : (int?)null; break; case Tags.SubsOnly: - SubOnly = Common.Helpers.ConvertToBool(tagValue); + SubOnly = TagHelper.ToBool(tagValue); break; case Tags.FollowersOnly: - if(int.TryParse(tagValue, out int minutes) && minutes > -1) + if (int.TryParse(tagValue, out int minutes)) { - FollowersOnly = TimeSpan.FromMinutes(minutes); + FollowersOnly = minutes > -1 ? TimeSpan.FromMinutes(minutes) : Timeout.InfiniteTimeSpan; } break; case Tags.RoomId: RoomId = tagValue; break; case Tags.Mercury: - Mercury = Common.Helpers.ConvertToBool(tagValue); + Mercury = TagHelper.ToBool(tagValue); break; default: - Console.WriteLine("[TwitchLib][ChannelState] Unaccounted for: " + tag); + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); break; } } Channel = ircMessage.Channel; } + /// + /// Initializes a new instance of the class. + /// public ChannelState( bool r9k, bool rituals, diff --git a/TwitchLib.Client.Models/ChatCommand.cs b/TwitchLib.Client.Models/ChatCommand.cs deleted file mode 100644 index b53b07c1..00000000 --- a/TwitchLib.Client.Models/ChatCommand.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TwitchLib.Client.Models -{ - /// Object representing a command received via Twitch chat. - public class ChatCommand - { - /// Property representing all arguments received in a List form. - public List ArgumentsAsList { get; } - - /// Property representing all arguments received in a string form. - public string ArgumentsAsString { get; } - - /// Property representing the chat message that the command came in. - public ChatMessage ChatMessage { get; } - - /// Property representing the command identifier (ie command prefix). - public char CommandIdentifier { get; } - - /// Property representing the actual command (without the command prefix). - public string CommandText { get; } - - /// ChatCommand constructor. - /// - public ChatCommand(ChatMessage chatMessage) - { - ChatMessage = chatMessage; - CommandText = chatMessage.Message.Split(' ')?[0].Substring(1, chatMessage.Message.Split(' ')[0].Length - 1) ?? chatMessage.Message.Substring(1, chatMessage.Message.Length - 1); ; - ArgumentsAsString = chatMessage.Message.Contains(" ") ? chatMessage.Message.Replace(chatMessage.Message.Split(' ')?[0] + " ", "") : ""; - if (!chatMessage.Message.Contains("\"") || chatMessage.Message.Count(x => x == '"') % 2 == 1) - ArgumentsAsList = chatMessage.Message.Split(' ')?.Where(arg => arg != chatMessage.Message[0] + CommandText).ToList() ?? new List(); - else - ArgumentsAsList = Common.Helpers.ParseQuotesAndNonQuotes(ArgumentsAsString); - CommandIdentifier = chatMessage.Message[0]; - } - - public ChatCommand( - ChatMessage chatMessage, - string commandText, - string argumentsAsString, - List argumentsAsList, - char commandIdentifier) - { - ChatMessage = chatMessage; - CommandText = commandText; - ArgumentsAsString = argumentsAsString; - ArgumentsAsList = argumentsAsList; - CommandIdentifier = commandIdentifier; - } - } -} diff --git a/TwitchLib.Client.Models/ChatMessage.cs b/TwitchLib.Client.Models/ChatMessage.cs index 4f30f848..c6deca40 100644 --- a/TwitchLib.Client.Models/ChatMessage.cs +++ b/TwitchLib.Client.Models/ChatMessage.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; -using TwitchLib.Client.Models.Extensions.NetCore; +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Extensions; using TwitchLib.Client.Models.Internal; namespace TwitchLib.Client.Models @@ -11,10 +7,13 @@ namespace TwitchLib.Client.Models /// Class represents ChatMessage in a Twitch channel. public class ChatMessage : TwitchLibMessage { - protected readonly MessageEmoteCollection _emoteCollection; + readonly ChatReply? _chatReply; + readonly HypeChat? _hypeChat; + + protected readonly MessageEmoteCollection? _emoteCollection; /// Information associated with badges. Not all badges will be in this list. Use carefully. - public List> BadgeInfo { get; } + public List> BadgeInfo { get; } = default!; /// If viewer sent bits in their message, total amount will be here. public int Bits { get; } @@ -26,16 +25,16 @@ public class ChatMessage : TwitchLibMessage public string Channel { get; } /// If a cheer badge exists, this property represents the raw value and color (more later). Can be null. - public CheerBadge CheerBadge { get; } + public CheerBadge? CheerBadge { get; } /// If a custom reward is present with the message, the ID will be set (null by default) - public string CustomRewardId { get; } + public string? CustomRewardId { get; } /// Text after emotes have been handled (if desired). Will be null if replaceEmotes is false. - public string EmoteReplacedMessage { get; } + public string? EmoteReplacedMessage { get; } /// Unique message identifier assigned by Twitch - public string Id { get; } + public string Id { get; } = default!; /// Chat message from broadcaster identifier flag public bool IsBroadcaster { get; } @@ -49,24 +48,9 @@ public class ChatMessage : TwitchLibMessage /// Chat message /me identifier flag. public bool IsMe { get; } - /// Channel specific moderator status. - public bool IsModerator { get; } - /// Message used channel points to skip sub mode public bool IsSkippingSubMode { get; internal set; } - /// Channel specific subscriber status. - public bool IsSubscriber { get; } - - /// Message is from channel VIP - public bool IsVip { get; } - - /// Message is from a Twitch Staff member - public bool IsStaff { get; } - - /// Message is from a Twitch Partner - public bool IsPartner { get; } - /// Twitch chat message contents. public string Message { get; } @@ -74,16 +58,19 @@ public class ChatMessage : TwitchLibMessage public Noisy Noisy { get; } /// Unique identifier of chat room. - public string RoomId { get; } + public string RoomId { get; } = default!; /// Number of months a person has been subbed. public int SubscribedMonthCount { get; } /// Sent timestamp generated by TMI - public string TmiSentTs { get; } + public DateTimeOffset TmiSent { get; } - // Chat reply information. Will be null if it is not a reply. - public ChatReply ChatReply { get; } + /// Chat reply information. Will be null if it is not a reply. + public ChatReply? ChatReply => _chatReply; + + /// Hype Chat information. + public HypeChat? HypeChat => _hypeChat; //Example IRC message: @badges=moderator/1,warcraft/alliance;color=;display-name=Swiftyspiffyv4;emotes=;mod=1;room-id=40876073;subscriber=0;turbo=0;user-id=103325214;user-type=mod :swiftyspiffyv4!swiftyspiffyv4@swiftyspiffyv4.tmi.twitch.tv PRIVMSG #swiftyspiffy :asd /// Constructor for ChatMessage object. @@ -91,11 +78,15 @@ public class ChatMessage : TwitchLibMessage /// The IRC message from Twitch to be processed. /// The to register new emotes on and, if desired, use for emote replacement. /// Whether to replace emotes for this chat message. Defaults to false. + /// Adds prefix to replaced emotes. Defaults to empty string. + /// Adds prefix to replaced emotes. Defaults to empty string. public ChatMessage( string botUsername, IrcMessage ircMessage, - ref MessageEmoteCollection emoteCollection, - bool replaceEmotes = false) + MessageEmoteCollection? emoteCollection = null, + bool replaceEmotes = false, + string prefix = "", + string suffix = "") { BotUsername = botUsername; RawIrcMessage = ircMessage.ToString(); @@ -119,14 +110,14 @@ public ChatMessage( Username = ircMessage.User; Channel = ircMessage.Channel; - foreach (var tag in ircMessage.Tags.Keys) + var userDetails = UserDetails.None; + foreach (var tag in ircMessage.Tags) { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) + var (tagKey, tagValue) = (tag.Key, tag.Value); + switch (tagKey) { case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); + Badges = TagHelper.ToBadges(tagValue); // Iterate through saved badges for special circumstances foreach (var badge in Badges) { @@ -137,36 +128,25 @@ public ChatMessage( break; case "subscriber": // Prioritize BadgeInfo subscribe count, as its more accurate - if(SubscribedMonthCount == 0) + if (SubscribedMonthCount == 0) { SubscribedMonthCount = int.Parse(badge.Value); } break; - case "vip": - IsVip = true; + default: break; - case "admin": - IsStaff = true; - break; - case "staff": - IsStaff = true; - break; - case "partner": - IsPartner = true; - break; - } } break; case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); + BadgeInfo = TagHelper.ToBadges(tagValue); // check if founder is one of them, and get months from that var founderBadge = BadgeInfo.Find(b => b.Key == "founder"); - if(!founderBadge.Equals(default(KeyValuePair))) + if (!founderBadge.Equals(default(KeyValuePair))) { - IsSubscriber = true; SubscribedMonthCount = int.Parse(founderBadge.Value); - } else + } + else { var subBadge = BadgeInfo.Find(b => b.Key == "subscriber"); // BadgeInfo has better accuracy than Badges subscriber value @@ -181,9 +161,7 @@ public ChatMessage( BitsInDollars = ConvertBitsToUsd(Bits); break; case Tags.Color: - ColorHex = tagValue; - if (!string.IsNullOrWhiteSpace(ColorHex)) - Color = ColorTranslator.FromHtml(ColorHex); + HexColor = tagValue; break; case Tags.CustomRewardId: CustomRewardId = tagValue; @@ -201,80 +179,47 @@ public ChatMessage( Id = tagValue; break; case Tags.MsgId: - handleMsgId(tagValue); + HandleMsgId(tagValue); break; case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Moderator; break; case Tags.Noisy: - Noisy = Common.Helpers.ConvertToBool(tagValue) ? Noisy.True : Noisy.False; - break; - case Tags.ReplyParentDisplayName: - if (ChatReply == null) { ChatReply = new ChatReply(); } // ChatReply is null if not reply - ChatReply.ParentDisplayName = tagValue; - break; - case Tags.ReplyParentMsgBody: - if (ChatReply == null) { ChatReply = new ChatReply(); } // ChatReply is null if not reply - ChatReply.ParentMsgBody = tagValue; - break; - case Tags.ReplyParentMsgId: - if (ChatReply == null) { ChatReply = new ChatReply(); } // ChatReply is null if not reply - ChatReply.ParentMsgId = tagValue; - break; - case Tags.ReplyParentUserId: - if (ChatReply == null) { ChatReply = new ChatReply(); } // ChatReply is null if not reply - ChatReply.ParentUserId = tagValue; - break; - case Tags.ReplyParentUserLogin: - if (ChatReply == null) { ChatReply = new ChatReply(); } // ChatReply is null if not reply - ChatReply.ParentUserLogin = tagValue; + Noisy = TagHelper.ToBool(tagValue) ? Noisy.True : Noisy.False; break; case Tags.RoomId: RoomId = tagValue; break; case Tags.Subscriber: - // this check because when founder is set, the subscriber value is actually 0, which is problematic - IsSubscriber = IsSubscriber == false ? Common.Helpers.ConvertToBool(tagValue) : true; + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Subscriber; break; case Tags.TmiSentTs: - TmiSentTs = tagValue; + TmiSent = TagHelper.ToDateTimeOffsetFromUnixMs(tagValue); break; case Tags.Turbo: - IsTurbo = Common.Helpers.ConvertToBool(tagValue); + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Turbo; break; case Tags.UserId: UserId = tagValue; break; case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - IsStaff = true; - break; - case "staff": - UserType = UserType.Staff; - IsStaff = true; - break; - default: - UserType = UserType.Viewer; - break; - } + UserType = TagHelper.ToUserType(tagValue); + break; + default: + if (!(ChatReply.TrySetTag(ref _chatReply, tag) || HypeChat.TrySetTag(ref _hypeChat, tag))) + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); break; } } + UserDetail = new(userDetails, Badges); //Parse the emoteSet - if (EmoteSet != null && Message != null && EmoteSet.Emotes.Count > 0) + if (_emoteCollection != null && EmoteSet?.Emotes.Count > 0) { - var uniqueEmotes = EmoteSet.RawEmoteSetString.Split('/'); - foreach (var emote in uniqueEmotes) + foreach (var emote in new SpanSliceEnumerator(EmoteSet.RawEmoteSetString!, '/')) { var firstColon = emote.IndexOf(':'); var firstComma = emote.IndexOf(','); @@ -282,13 +227,20 @@ public ChatMessage( var firstDash = emote.IndexOf('-'); if (firstColon > 0 && firstDash > firstColon && firstComma > firstDash) { - if (int.TryParse(emote.Substring(firstColon + 1, firstDash - firstColon - 1), out var low) && - int.TryParse(emote.Substring(firstDash + 1, firstComma - firstDash - 1), out var high)) +#if NETSTANDARD2_0 + var lowStr = emote.Slice(firstColon + 1, firstDash - firstColon - 1).ToString(); + var highStr = emote.Slice(firstDash + 1, firstComma - firstDash - 1).ToString(); +#else + var lowStr = emote.Slice(firstColon + 1, firstDash - firstColon - 1); + var highStr = emote.Slice(firstDash + 1, firstComma - firstDash - 1); +#endif + if (int.TryParse(lowStr, out var low) && + int.TryParse(highStr, out var high)) { if (low >= 0 && low < high && high < Message.Length) { //Valid emote, let's parse - var id = emote.Substring(0, firstColon); + var id = emote.Slice(0, firstColon).ToString(); //Pull the emote text from the message var text = Message.Substring(low, high - low + 1); _emoteCollection.Add(new MessageEmote(id, text)); @@ -298,12 +250,11 @@ public ChatMessage( } if (replaceEmotes) { - EmoteReplacedMessage = _emoteCollection.ReplaceEmotes(Message); + EmoteReplacedMessage = _emoteCollection?.ReplaceEmotes(Message, prefix: prefix, suffix: suffix); } } - if (EmoteSet == null) - EmoteSet = new EmoteSet(default(string), Message); + EmoteSet ??= new EmoteSet(default(string), Message); // Check if display name was set, and if it wasn't, set it to username if (string.IsNullOrEmpty(DisplayName)) @@ -316,66 +267,55 @@ public ChatMessage( IsBroadcaster = true; } - if (Channel.Split(':').Length == 3) + var splitData = Channel.Split(':'); + if (splitData.Length == 3 && + string.Equals(splitData[1], UserId, StringComparison.InvariantCultureIgnoreCase)) { - if (string.Equals(Channel.Split(':')[1], UserId, StringComparison.InvariantCultureIgnoreCase)) - { - UserType = UserType.Broadcaster; - IsBroadcaster = true; - } + UserType = UserType.Broadcaster; + IsBroadcaster = true; } } + /// + /// Initializes a new instance of the class. + /// public ChatMessage( string botUsername, string userId, string userName, string displayName, - string colorHex, - Color color, + string hexColor, EmoteSet emoteSet, string message, UserType userType, string channel, string id, - bool isSubscriber, int subscribedMonthCount, string roomId, - bool isTurbo, - bool isModerator, bool isMe, bool isBroadcaster, - bool isVip, - bool isPartner, - bool isStaff, Noisy noisy, string rawIrcMessage, string emoteReplacedMessage, List> badges, CheerBadge cheerBadge, int bits, - double bitsInDollars) + double bitsInDollars, + UserDetail userDetail) { BotUsername = botUsername; UserId = userId; DisplayName = displayName; - ColorHex = colorHex; - Color = color; + HexColor = hexColor; EmoteSet = emoteSet; Message = message; UserType = userType; Channel = channel; Id = id; - IsSubscriber = isSubscriber; SubscribedMonthCount = subscribedMonthCount; RoomId = roomId; - IsTurbo = isTurbo; - IsModerator = isModerator; IsMe = isMe; IsBroadcaster = isBroadcaster; - IsVip = isVip; - IsPartner = isPartner; - IsStaff = isStaff; Noisy = noisy; RawIrcMessage = rawIrcMessage; EmoteReplacedMessage = emoteReplacedMessage; @@ -384,17 +324,18 @@ public ChatMessage( Bits = bits; BitsInDollars = bitsInDollars; Username = userName; + UserDetail = userDetail; } - private void handleMsgId(string val) + private void HandleMsgId(string val) { - switch(val) { - case MsgIds.HighlightedMessage: - IsHighlighted = true; - break; - case MsgIds.SkipSubsModeMessage: - IsSkippingSubMode = true; - break; + if (val == MsgIds.HighlightedMessage) + { + IsHighlighted = true; + } + else if (val == MsgIds.SkipSubsModeMessage) + { + IsSkippingSubMode = true; } } @@ -409,23 +350,14 @@ Conversion Rates 10000 bits = $126.00 (10%) 25000 bits = $308.00 (12%) */ - if (bits < 1500) + return bits switch { - return (double)bits / 100 * 1.4; - } - if (bits < 5000) - { - return (double)bits / 1500 * 19.95; - } - if (bits < 10000) - { - return (double)bits / 5000 * 64.40; - } - if (bits < 25000) - { - return (double)bits / 10000 * 126; - } - return (double)bits / 25000 * 308; + < 1500 => (double)bits / 100 * 1.4, + < 5000 => (double)bits / 1500 * 19.95, + < 10000 => (double)bits / 5000 * 64.40, + < 25000 => (double)bits / 10000 * 126, + _ => (double)bits / 25000 * 308 + }; } } } diff --git a/TwitchLib.Client.Models/ChatReply.cs b/TwitchLib.Client.Models/ChatReply.cs index c25948ad..7dd76a0a 100644 --- a/TwitchLib.Client.Models/ChatReply.cs +++ b/TwitchLib.Client.Models/ChatReply.cs @@ -1,25 +1,74 @@ -using System; -using System.Collections.Generic; -using System.Text; +using TwitchLib.Client.Models.Internal; namespace TwitchLib.Client.Models { /// Class representing a chat reply/thread public class ChatReply { - /// Property representing the display name of the responded to message - public string ParentDisplayName { get; internal set; } + /// + /// The display name of the sender of the direct parent message. + /// + public string ParentDisplayName { get; internal set; } = default!; - /// Property representing the message contents of the responded to message - public string ParentMsgBody { get; internal set; } + /// + /// The text of the direct parent message. + /// + public string ParentMsgBody { get; internal set; } = default!; - /// Property representing the id of the responded to message - public string ParentMsgId { get; internal set; } + /// + /// An ID that uniquely identifies the direct parent message that this message is replying to. + /// + public string ParentMsgId { get; internal set; } = default!; - /// Property representing the user id of the sender of the responded to message - public string ParentUserId { get; internal set; } + /// + /// An ID that identifies the sender of the direct parent message. + /// + public string ParentUserId { get; internal set; } = default!; - /// Property representing the user login of the sender of the responded to message - public string ParentUserLogin { get; internal set; } + /// + /// The login name of the sender of the direct parent message. + /// + public string ParentUserLogin { get; internal set; } = default!; + + /// + /// An ID that uniquely identifies the top-level parent message of the reply thread that this message is replying to. + /// + public string ThreadParentMsgId { get; internal set; } = default!; + + /// + /// The login name of the sender of the top-level parent message. + /// + public string ThreadParentUserLogin { get; internal set; } = default!; + + internal static bool TrySetTag(ref ChatReply? reply, KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.ReplyParentDisplayName: + (reply ??= new()).ParentDisplayName = tag.Value; + break; + case Tags.ReplyParentMsgBody: + (reply ??= new()).ParentMsgBody = tag.Value; + break; + case Tags.ReplyParentMsgId: + (reply ??= new()).ParentMsgId = tag.Value; + break; + case Tags.ReplyParentUserId: + (reply ??= new()).ParentUserId = tag.Value; + break; + case Tags.ReplyParentUserLogin: + (reply ??= new()).ParentUserLogin = tag.Value; + break; + case Tags.ReplyThreadParentMsgId: + (reply ??= new()).ThreadParentMsgId = tag.Value; + break; + case Tags.ReplyThreadParentUserLogin: + (reply ??= new()).ThreadParentUserLogin = tag.Value; + break; + default: + return false; + } + return true; + } } } diff --git a/TwitchLib.Client.Models/CommandInfo.cs b/TwitchLib.Client.Models/CommandInfo.cs new file mode 100644 index 00000000..889664f6 --- /dev/null +++ b/TwitchLib.Client.Models/CommandInfo.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; + +namespace TwitchLib.Client.Models; + +/// Object representing a command received via Twitch chat. +public class CommandInfo +{ + /// Property representing the command identifier (ie command prefix). + public string Identifier { get; } + + /// Property representing the actual command (without the command prefix). + public string Name { get; } + + /// Property representing all arguments received in a string form. + public string ArgumentsAsString { get; } + + /// Property representing all arguments received in a List form. + public List ArgumentsAsList { get; } + + /// + /// Initializes a new instance of the class. + /// + public CommandInfo(string identifier, string name) : this(identifier, name, string.Empty, new()) + { } + + /// + /// Initializes a new instance of the class. + /// + public CommandInfo(string identifier, string name, string argumentsAsString, List argumentsAsList) + { + Identifier = identifier; + Name = name; + ArgumentsAsString = argumentsAsString; + ArgumentsAsList = argumentsAsList; + } + + /// + /// Tries to parse a message with specified command identifier into a value. + /// + /// true if s was successfully parsed; otherwise, false. +#if NETSTANDARD2_0 + internal static bool TryParse(string commandIdentifier, ReadOnlySpan message, out CommandInfo result) +#else + internal static bool TryParse(string commandIdentifier, ReadOnlySpan message, [MaybeNullWhen(false)] out CommandInfo result) +#endif + { + result = default!; + if(!message.StartsWith(commandIdentifier.AsSpan())) + return false; + + 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 = message.IndexOf(' '); + if (indexOfSpace == -1) + { + var name = message.ToString(); + result = new(commandIdentifier, name); + } + else + { + 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; + + static List ParseArgumentsToList(ReadOnlySpan s) + { + int index; + var arguments = new List(); + while (!s.IsEmpty) + { + bool isQuote = s[0] == '"'; + if (s[0] == '"') + { + s = s.Slice(1); + index = s.IndexOf('"'); + } + else + { + index = s.IndexOfAny('"', ' '); + } + if (index == -1) + { + arguments.Add(s.ToString()); + s = default; + } + else + { + arguments.Add(s.Slice(0, index).ToString()); + if (!isQuote && s[index] == '"') // s"txt" we dont want remove quote after s + index--; + s = s.Slice(index + 1); + } + s = s.TrimStart(); + } + return arguments; + } + } + + /// + public override string ToString() + { + return ArgumentsAsString.Length == 0 + ? $"{Identifier}{Name}" + : $"{Identifier}{Name} {ArgumentsAsString}"; + } +} diff --git a/TwitchLib.Client.Models/Common/Helpers.cs b/TwitchLib.Client.Models/Common/Helpers.cs deleted file mode 100644 index 4e9094ed..00000000 --- a/TwitchLib.Client.Models/Common/Helpers.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace TwitchLib.Client.Models.Common -{ - /// Static class of helper functions used around the project. - public static class Helpers - { - /// - /// Parses out strings that have quotes, ideal for commands that use quotes for parameters - /// - /// Input string to attempt to parse. - /// List of contents of quotes from the input string - public static List ParseQuotesAndNonQuotes(string message) - { - var args = new List(); - - // Return if empty string - if (message == "") - return new List(); - - var previousQuoted = message[0] != '"'; - // Parse quoted text as a single argument - foreach (var arg in message.Split('"')) - { - if (string.IsNullOrEmpty(arg)) - continue; - - // This arg is a quoted arg, add it right away - if (!previousQuoted) - { - args.Add(arg); - previousQuoted = true; - continue; - } - - if (!arg.Contains(" ")) - continue; - - // This arg is non-quoted, iterate through each split and add it if it's not empty/whitespace - foreach (var dynArg in arg.Split(' ')) - { - if (string.IsNullOrWhiteSpace(dynArg)) - continue; - - args.Add(dynArg); - previousQuoted = false; - } - } - return args; - } - - /// - /// Parses the badges field in GLOBALUSERSTATE, PRIVMSG, USERNOTICE, USERSTATE, etc - /// - /// The data. - /// List of keyvalue pairs representing each badge and value associated - public static List> ParseBadges(string badgesStr) - { - var badges = new List>(); - - if (badgesStr.Contains('/')) - { - if (!badgesStr.Contains(",")) - badges.Add(new KeyValuePair(badgesStr.Split('/')[0], badgesStr.Split('/')[1])); - else - foreach (var badge in badgesStr.Split(',')) - badges.Add(new KeyValuePair(badge.Split('/')[0], badge.Split('/')[1])); - } - - return badges; - } - - public static string ParseToken(string token, string message) - { - var tokenValue = string.Empty; - - for (var i = message.IndexOf(token, StringComparison.InvariantCultureIgnoreCase); - i > -1; - i = message.IndexOf(token, i + token.Length, StringComparison.InvariantCultureIgnoreCase)) - { - tokenValue = new string(message - .Substring(i) - .TakeWhile(x => x != ';' && x != ' ') - .ToArray()) - .Split('=') - .LastOrDefault(); - } - - return tokenValue; - } - - public static bool ConvertToBool(string data) - { - return data == "1"; - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/CommunityPayForward.cs b/TwitchLib.Client.Models/CommunityPayForward.cs new file mode 100644 index 00000000..b1fa1d4d --- /dev/null +++ b/TwitchLib.Client.Models/CommunityPayForward.cs @@ -0,0 +1,90 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class CommunityPayForward : UserNoticeBase +{ + public bool MsgParamPriorGifterAnonymous { get; protected set; } + + public string MsgParamPriorGifterDisplayName { get; protected set; } = default!; + + public string MsgParamPriorGifterId { get; protected set; } = default!; + + public string MsgParamPriorGifterUserName { get; protected set; } = default!; + + /// + /// Initializes a new instance of the class. + /// + public CommunityPayForward(IrcMessage ircMessage) : base(ircMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public CommunityPayForward( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + bool msgParamPriorGifterAnonymous, + string msgParamPriorGifterDisplayName, + string msgParamPriorGifterId, + string msgParamPriorGifterUserName) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamPriorGifterAnonymous = msgParamPriorGifterAnonymous; + MsgParamPriorGifterDisplayName = msgParamPriorGifterDisplayName; + MsgParamPriorGifterId = msgParamPriorGifterId; + MsgParamPriorGifterUserName = msgParamPriorGifterUserName; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamPriorGifterAnonymous: + MsgParamPriorGifterAnonymous = bool.Parse(tag.Value); + break; + case Tags.MsgParamPriorGifterDisplayName: + MsgParamPriorGifterDisplayName = tag.Value; + break; + case Tags.MsgParamPriorGifterId: + MsgParamPriorGifterId = tag.Value; + break; + case Tags.MsgParamPriorGifterUserName: + MsgParamPriorGifterUserName = tag.Value; + break; + default: + return false; + } + return true; + } +} diff --git a/TwitchLib.Client.Models/CommunitySubscription.cs b/TwitchLib.Client.Models/CommunitySubscription.cs index ce7b4ffc..3787f0a5 100644 --- a/TwitchLib.Client.Models/CommunitySubscription.cs +++ b/TwitchLib.Client.Models/CommunitySubscription.cs @@ -1,146 +1,108 @@ -using System; -using System.Collections.Generic; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +//submysterygift +public class CommunitySubscription : UserNoticeBase { - public class CommunitySubscription + private Goal? _goal; + + public bool IsAnonymous { get; } + + public Goal? MsgParamGoal { get => _goal; protected set => _goal = value; } + + public string MsgParamGiftTheme { get; protected set; } = default!; + + public int MsgParamMassGiftCount { get; protected set; } + + public string MsgParamOriginId { get; protected set; } = default!; + + public int MsgParamSenderCount { get; protected set; } + + /// + /// The type of subscription plan being used. + /// + public SubscriptionPlan MsgParamSubPlan { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public CommunitySubscription(IrcMessage ircMessage) : base(ircMessage) { - private const string AnonymousGifterUserId = "274598607"; + IsAnonymous = UserId == AnonymousGifterUserId; + } - public List> Badges; - public List> BadgeInfo; - public string Color; - public string DisplayName; - public string Emotes; - public string Id; - public string Login; - public bool IsModerator; - public bool IsAnonymous; - public string MsgId; - public int MsgParamMassGiftCount; - public int MsgParamSenderCount; - public SubscriptionPlan MsgParamSubPlan; - public string RoomId; - public bool IsSubscriber; - public string SystemMsg; - public string SystemMsgParsed; - public string TmiSentTs; - public bool IsTurbo; - public string UserId; - public UserType UserType; - public string MsgParamMultiMonthGiftDuration; + /// + /// Initializes a new instance of the class. + /// + public CommunitySubscription( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + Goal? msgParamGoal, + string msgParamGiftTheme, + int msgParamMassGiftCount, + string msgParamOriginId, + int msgParamSenderCount, + SubscriptionPlan msgParamSubPlan) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + IsAnonymous = userId == AnonymousGifterUserId; + MsgParamGoal = msgParamGoal; + MsgParamGiftTheme = msgParamGiftTheme; + MsgParamMassGiftCount = msgParamMassGiftCount; + MsgParamOriginId = msgParamOriginId; + MsgParamSenderCount = msgParamSenderCount; + MsgParamSubPlan = msgParamSubPlan; + } - public CommunitySubscription(IrcMessage ircMessage) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - Color = tagValue; - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - Emotes = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.MsgParamSubPlan: - switch (tagValue) - { - case "prime": - MsgParamSubPlan = SubscriptionPlan.Prime; - break; - case "1000": - MsgParamSubPlan = SubscriptionPlan.Tier1; - break; - case "2000": - MsgParamSubPlan = SubscriptionPlan.Tier2; - break; - case "3000": - MsgParamSubPlan = SubscriptionPlan.Tier3; - break; - default: - throw new ArgumentOutOfRangeException(nameof(tagValue.ToLower)); - } - break; - case Tags.MsgParamMassGiftCount: - MsgParamMassGiftCount = int.Parse(tagValue); - break; - case Tags.MsgParamSenderCount: - MsgParamSenderCount = int.Parse(tagValue); - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - IsSubscriber = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.SystemMsg: - SystemMsg = tagValue; - SystemMsgParsed = tagValue.Replace("\\s", " ").Replace("\\n", ""); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.Turbo: - IsTurbo = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.UserId: - UserId = tagValue; - if(UserId == AnonymousGifterUserId) - { - IsAnonymous = true; - } - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - case Tags.MsgParamMultiMonthGiftDuration: - MsgParamMultiMonthGiftDuration = tagValue; - break; - } - } + case Tags.MsgParamGiftTheme: + MsgParamGiftTheme = tag.Value; + break; + case Tags.MsgParamMassGiftCount: + MsgParamMassGiftCount = int.Parse(tag.Value); + break; + case Tags.MsgParamSenderCount: + MsgParamSenderCount = int.Parse(tag.Value); + break; + case Tags.MsgParamSubPlan: + MsgParamSubPlan = TagHelper.ToSubscriptionPlan(tag.Value); + break; + default: + return Goal.TrySetTag(ref _goal, tag); } + return true; } } diff --git a/TwitchLib.Client.Models/ConnectionCredentials.cs b/TwitchLib.Client.Models/ConnectionCredentials.cs index cefa5e2c..b8adaa45 100644 --- a/TwitchLib.Client.Models/ConnectionCredentials.cs +++ b/TwitchLib.Client.Models/ConnectionCredentials.cs @@ -1,16 +1,17 @@ -using System; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace TwitchLib.Client.Models { /// Class used to store credentials used to connect to Twitch chat/whisper. - public class ConnectionCredentials + public partial class ConnectionCredentials { - public const string DefaultWebSocketUri = "wss://irc-ws.chat.twitch.tv:443"; - - /// Property representing URI used to connect to Twitch websocket service. - public string TwitchWebsocketURI { get; } - +#if NET7_0_OR_GREATER + [GeneratedRegex("^([a-zA-Z0-9][a-zA-Z0-9_]{4,25})$")] + private static partial Regex GetUsernameCheckRegex(); +#else + private static Regex GetUsernameCheckRegex() => UsernameCheckRegex; + private static readonly Regex UsernameCheckRegex = new("^([a-zA-Z0-9][a-zA-Z0-9_]{4,25})$"); +#endif /// Property representing bot's oauth. public string TwitchOAuth { get; } @@ -24,27 +25,32 @@ public class ConnectionCredentials public ConnectionCredentials( string twitchUsername, string twitchOAuth, - string twitchWebsocketURI = DefaultWebSocketUri, bool disableUsernameCheck = false, - Capabilities capabilities = null) + Capabilities? capabilities = null) { - if (!disableUsernameCheck && !new Regex("^([a-zA-Z0-9][a-zA-Z0-9_]{3,25})$").Match(twitchUsername).Success) + if (!disableUsernameCheck && !GetUsernameCheckRegex().Match(twitchUsername).Success) + { throw new Exception($"Twitch username does not appear to be valid. {twitchUsername}"); + } TwitchUsername = twitchUsername.ToLower(); TwitchOAuth = twitchOAuth; // Make sure proper formatting is applied to oauth - if (!twitchOAuth.Contains(":")) + if (!TwitchOAuth.Contains(":")) { - TwitchOAuth = $"oauth:{twitchOAuth.Replace("oauth", "")}"; + TwitchOAuth = $"oauth:{TwitchOAuth.Replace("oauth", "")}"; } - TwitchWebsocketURI = twitchWebsocketURI; + Capabilities = capabilities ?? new Capabilities(); + } - if (capabilities == null) - capabilities = new Capabilities(); - Capabilities = capabilities; + /// Constructor for ConnectionCredentials object as anonymous user. + public ConnectionCredentials(Capabilities? capabilities = null) + { + TwitchUsername = $"justinfan{new Random().Next(1000, 89999)}"; + TwitchOAuth = "oauth:"; + Capabilities = capabilities ?? new Capabilities(); } } @@ -60,6 +66,9 @@ public class Capabilities /// Enables several Twitch-specific commands. public bool Commands { get; } + /// + /// Initializes a new instance of the class. + /// public Capabilities(bool membership = true, bool tags = true, bool commands = true) { Membership = membership; diff --git a/TwitchLib.Client.Models/ContinuedGiftedSubscription.cs b/TwitchLib.Client.Models/ContinuedGiftedSubscription.cs index a2ce8e22..d6d76eb3 100644 --- a/TwitchLib.Client.Models/ContinuedGiftedSubscription.cs +++ b/TwitchLib.Client.Models/ContinuedGiftedSubscription.cs @@ -1,132 +1,104 @@ -using System; -using System.Collections.Generic; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +// giftpaidupgrade +public class ContinuedGiftedSubscription : UserNoticeBase { - public class ContinuedGiftedSubscription + /// + /// The number of gifts the gifter has given during the promo indicated by . + /// + public int MsgParamPromoGiftTotal { get; protected set; } + + /// + /// The subscriptions promo, if any, that is ongoing (for example, Subtember 2018). + /// + public string MsgParamPromoName { get; protected set; } = default!; + + /// + /// The login name of the user who gifted the subscription. + /// + public string MsgParamSenderLogin { get; protected set; } = default!; + + /// + /// The display name of the user who gifted the subscription. + /// + public string MsgParamSenderName { get; protected set; } = default!; + + + /// + /// Initializes a new instance of the class. + /// + public ContinuedGiftedSubscription(IrcMessage ircMessage) : base(ircMessage) { - //@badge-info=subscriber/11;badges=subscriber/9;color=#DAA520;display-name=Varanid;emotes=;flags=;id=a2d384c1-c30a-409e-8001-9e7d8f9c784d;login=varanid;mod=0;msg-id=giftpaidupgrade;msg-param-sender-login=cletusbueford;msg-param-sender-name=CletusBueford;room-id=44338537;subscriber=1;system-msg=Varanid\sis\scontinuing\sthe\sGift\sSub\sthey\sgot\sfrom\sCletusBueford!;tmi-sent-ts=1612497386372;user-id=67505836;user-type= :tmi.twitch.tv USERNOTICE #burkeblack - - public List> Badges { get; } - - public List> BadgeInfo { get; } - - public string Color { get; } - - public string DisplayName { get; } - - public string Emotes { get; } - - public string Flags { get; } - - public string Id { get; } - - public string Login { get; } - - public bool IsModerator { get; } - - public string MsgId { get; } - - public string MsgParamSenderLogin { get; } - - public string MsgParamSenderName { get; } - - public string RoomId { get; } - - public bool IsSubscriber { get; } - - public string SystemMsg { get; } - - public string TmiSentTs { get; } - - public string UserId { get; } + } - public UserType UserType { get; } + /// + /// Initializes a new instance of the class. + /// + public ContinuedGiftedSubscription( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + int msgParamPromoGiftTotal, + string msgParamPromoName, + string msgParamSenderLogin, + string msgParamSenderName) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamPromoGiftTotal = msgParamPromoGiftTotal; + MsgParamPromoName = msgParamPromoName; + MsgParamSenderLogin = msgParamSenderLogin; + MsgParamSenderName = msgParamSenderName; + } - public ContinuedGiftedSubscription(IrcMessage ircMessage) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) - { - case Tags.SystemMsg: - SystemMsg = tagValue; - break; - case Tags.Flags: - Flags = tagValue; - break; - case Tags.MsgParamSenderLogin: - MsgParamSenderLogin = tagValue; - break; - case Tags.MsgParamSenderName: - MsgParamSenderName = tagValue; - break; - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - Color = tagValue; - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - Emotes = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - IsSubscriber = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.UserId: - UserId = tagValue; - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - } - } + case Tags.MsgParamPromoGiftTotal: + MsgParamPromoGiftTotal = int.Parse(tag.Value); + break; + case Tags.MsgParamPromoName: + MsgParamPromoName = tag.Value; + break; + case Tags.MsgParamSenderLogin: + MsgParamSenderLogin = tag.Value; + break; + case Tags.MsgParamSenderName: + MsgParamSenderName = tag.Value; + break; + default: + return false; } + return true; } } diff --git a/TwitchLib.Client.Models/EmoteSet.cs b/TwitchLib.Client.Models/EmoteSet.cs index dd8f1efe..35d64d6d 100644 --- a/TwitchLib.Client.Models/EmoteSet.cs +++ b/TwitchLib.Client.Models/EmoteSet.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; - -using TwitchLib.Client.Models.Extractors; +using TwitchLib.Client.Models.Extractors; // TODO: Builder is missing namespace TwitchLib.Client.Models @@ -13,25 +10,24 @@ public class EmoteSet public List Emotes { get; } /// The raw emote set string obtained from Twitch, for legacy purposes. - public string RawEmoteSetString { get; } + public string? RawEmoteSetString { get; } /// Constructor for ChatEmoteSet object. /// /// - public EmoteSet(string rawEmoteSetString, string message) + public EmoteSet(string? rawEmoteSetString, string message) { // this should be removed and used outside of object RawEmoteSetString = rawEmoteSetString; - EmoteExtractor emoteExtractor = new EmoteExtractor(); - Emotes = emoteExtractor.Extract(rawEmoteSetString, message).ToList(); + Emotes = EmoteExtractor.Extract(rawEmoteSetString, message); } /// Constructor for ChatEmoteSet object. /// Collection of Emote instances /// Original string from which emotes were created - public EmoteSet(IEnumerable emotes, string emoteSetData) + public EmoteSet(IEnumerable emotes, string rawEmoteSetString) { - RawEmoteSetString = emoteSetData; + RawEmoteSetString = rawEmoteSetString; Emotes = emotes.ToList(); } } diff --git a/TwitchLib.Client.Models/ErrorEvent.cs b/TwitchLib.Client.Models/ErrorEvent.cs index 085614f7..e1bcf34c 100644 --- a/TwitchLib.Client.Models/ErrorEvent.cs +++ b/TwitchLib.Client.Models/ErrorEvent.cs @@ -4,6 +4,14 @@ public class ErrorEvent { /// Message pertaining to the error. - public string Message { get; set; } + public string Message { get; } + + /// + /// Initializes a new instance of the class. + /// + public ErrorEvent(string message) + { + Message = message; + } } } diff --git a/TwitchLib.Client.Models/Extensions/NetCore/ColorTranslator.cs b/TwitchLib.Client.Models/Extensions/NetCore/ColorTranslator.cs deleted file mode 100644 index 0041e1da..00000000 --- a/TwitchLib.Client.Models/Extensions/NetCore/ColorTranslator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Drawing; -using System.Globalization; - -namespace TwitchLib.Client.Models.Extensions.NetCore -{ - public static class ColorTranslator - { - public static Color FromHtml(string hexColor) - { - hexColor = hexColor + 00; - var argb = int.Parse(hexColor.Replace("#", ""), NumberStyles.HexNumber); - return Color.FromArgb(argb); - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Extensions/SpanSliceEnumerator.cs b/TwitchLib.Client.Models/Extensions/SpanSliceEnumerator.cs new file mode 100644 index 00000000..e95c1709 --- /dev/null +++ b/TwitchLib.Client.Models/Extensions/SpanSliceEnumerator.cs @@ -0,0 +1,41 @@ +using System; + +namespace TwitchLib.Client.Models.Extensions; + +internal ref struct SpanSliceEnumerator +{ + ReadOnlySpan _span; + readonly char _char; + + public SpanSliceEnumerator(ReadOnlySpan span, char @char) + { + _span = span; + _char = @char; + } + + public SpanSliceEnumerator(string str, char @char) : this(str.AsSpan(), @char) + { } + + public ReadOnlySpan Current { get; private set; } + + public SpanSliceEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + if (_span.IsEmpty) + return false; + var index = _span.IndexOf(_char); + + if (index == -1) + { + Current = _span; + _span = default; + } + else + { + Current = _span.Slice(0, index); + _span = _span.Slice(index + 1); + } + return true; + } +} diff --git a/TwitchLib.Client.Models/Extractors/EmoteExtractor.cs b/TwitchLib.Client.Models/Extractors/EmoteExtractor.cs index 8cdff6bb..7e89eddf 100644 --- a/TwitchLib.Client.Models/Extractors/EmoteExtractor.cs +++ b/TwitchLib.Client.Models/Extractors/EmoteExtractor.cs @@ -1,81 +1,44 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Models.Builders; +using TwitchLib.Client.Models.Extensions; namespace TwitchLib.Client.Models.Extractors { - public class EmoteExtractor + public static class EmoteExtractor { - public IEnumerable Extract(string rawEmoteSetString, string message) + public static List Extract(string? rawEmoteSetString, string message) { - if (string.IsNullOrEmpty(rawEmoteSetString) - || string.IsNullOrEmpty(message)) - { - yield break; - } - - if (rawEmoteSetString.Contains("/")) + var emotes = new List(); + if (string.IsNullOrEmpty(rawEmoteSetString) || string.IsNullOrEmpty(message)) + return emotes; + + System.Globalization.StringInfo messageInfo = new(message); + // 25:5-9,28-32/28087:15-21 => 25:5-9,28-32 28087:15-21 +#pragma warning disable CS8604 // Possible null reference argument. false positiv in NS 2.0 + foreach (var emoteData in new SpanSliceEnumerator(rawEmoteSetString, '/')) +#pragma warning restore CS8604 // Possible null reference argument. { - // Message contains multiple different emotes, first parse by unique emotes: 28087:15-21/25:5-9,28-32 - foreach (var emoteData in rawEmoteSetString.Split('/')) + var index = emoteData.IndexOf(':'); + var emoteId = emoteData.Slice(0, index).ToString(); + // 25:5-9,28-32 => 5-9 28-32 + foreach (var emote in new SpanSliceEnumerator(emoteData.Slice(index + 1), ',')) { - var emoteId = emoteData.Split(':')[0]; - if (emoteData.Contains(",")) - { - // Multiple copies of a single emote: 25:5-9,28-32 - foreach (var emote in emoteData.Replace($"{emoteId}:", "").Split(',')) - yield return GetEmote(emote, emoteId, message); - } - else - { - // Single copy of single emote: 25:5-9/28087:16-22 - yield return GetEmote(emoteData, emoteId, message, true); - } + index = emote.IndexOf('-'); + var startSlice = emote.Slice(0, index); + var endSlice = emote.Slice(index + 1); +#if NETSTANDARD2_0 + var start = int.Parse(startSlice.ToString()); + var end = int.Parse(endSlice.ToString()); +#else + var start = int.Parse(startSlice); + var end = int.Parse(endSlice); +#endif + int trueStart = messageInfo.SubstringByTextElements(0, start + 1).Length - 1; + string name = messageInfo.SubstringByTextElements(start, end - start + 1); + int trueEnd = trueStart + name.Length - 1; + + emotes.Add(new(emoteId, name, trueStart, trueEnd)); } } - else - { - var emoteId = rawEmoteSetString.Split(':')[0]; - // Message contains a single, or multiple of the same emote - if (rawEmoteSetString.Contains(",")) - { - // Multiple copies of a single emote: 25:5-9,28-32 - foreach (var emote in rawEmoteSetString.Replace($"{emoteId}:", "").Split(',')) - yield return GetEmote(emote, emoteId, message); - } - else - { - // Single copy of single emote: 25:5-9 - yield return GetEmote(rawEmoteSetString, emoteId, message, true); - } - } - } - - private Emote GetEmote(string emoteData, string emoteId, string message, bool single = false) - { - int startIndex = -1; - int endIndex = -1; - - if (single) - { - startIndex = int.Parse(emoteData.Split(':')[1].Split('-')[0]); - endIndex = int.Parse(emoteData.Split(':')[1].Split('-')[1]); - } - else - { - startIndex = int.Parse(emoteData.Split('-')[0]); - endIndex = int.Parse(emoteData.Split('-')[1]); - } - - string name = message.Substring(startIndex, (endIndex - startIndex) + 1); - - EmoteBuilder emoteBuilder = EmoteBuilder.Create() - .WithId(emoteId) - .WithName(name) - .WithStartIndex(startIndex) - .WithEndIndex(endIndex); - - return emoteBuilder.Build(); + return emotes; } } } diff --git a/TwitchLib.Client.Models/Extractors/IExtractor.cs b/TwitchLib.Client.Models/Extractors/IExtractor.cs deleted file mode 100644 index a3786ab7..00000000 --- a/TwitchLib.Client.Models/Extractors/IExtractor.cs +++ /dev/null @@ -1,9 +0,0 @@ -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Models.Extractors -{ - public interface IExtractor - { - TResult Extract(IrcMessage ircMessage); - } -} diff --git a/TwitchLib.Client.Models/Extractors/UserDetailsExtractor.cs b/TwitchLib.Client.Models/Extractors/UserDetailsExtractor.cs new file mode 100644 index 00000000..aef9000b --- /dev/null +++ b/TwitchLib.Client.Models/Extractors/UserDetailsExtractor.cs @@ -0,0 +1,23 @@ +using TwitchLib.Client.Enums; + +namespace TwitchLib.Client.Models.Extractors; + +internal static class UserDetailsExtractor +{ + public static UserDetails Extract(List> badges) + { + var userDetails = UserDetails.None; + foreach (var item in badges) + { + userDetails |= item.Key switch + { + "founder" or "subscriber" => UserDetails.Subscriber, + "admin" or "staff" => UserDetails.Staff, + "partner" => UserDetails.Partner, + "vip" => UserDetails.Vip, + _ => default + }; + } + return userDetails; + } +} diff --git a/TwitchLib.Client.Models/GiftedSubscription.cs b/TwitchLib.Client.Models/GiftedSubscription.cs index b6f84faf..a55162e7 100644 --- a/TwitchLib.Client.Models/GiftedSubscription.cs +++ b/TwitchLib.Client.Models/GiftedSubscription.cs @@ -1,234 +1,157 @@ -using System; -using System.Collections.Generic; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +//SubGift +public class GiftedSubscription : UserNoticeBase { - public class GiftedSubscription + private Goal? _goal; + + public Goal? MsgParamGoal { get => _goal; protected set => _goal = value; } + + public bool IsAnonymous { get; } + + /// + /// The total number of months the user has subscribed. + /// + public string MsgParamMonths { get; protected set; } = default!; + + /// + /// If this message sourced from an event, this is the ID of that event + /// + public string MsgParamOriginId { get; protected set; } + + /// + /// The display name of the subscription gift recipient. + /// + public string MsgParamRecipientDisplayName { get; protected set; } = default!; + + /// + /// The user ID of the subscription gift recipient. + /// + public string MsgParamRecipientId { get; protected set; } = default!; + + /// + /// The user name of the subscription gift recipient. + /// + public string MsgParamRecipientUserName { get; protected set; } = default!; + + public int MsgParamSenderCount { get; protected set; } + + /// + /// The type of subscription plan being used. + /// + public SubscriptionPlan MsgParamSubPlan { get; protected set; } + + /// + /// The display name of the subscription plan. This may be a default name or one created by the channel owner. + /// + public string MsgParamSubPlanName { get; protected set; } = default!; + + /// + /// The number of months gifted as part of a single, multi-month gift. + /// + public int MsgParamMultiMonthGiftDuration { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public GiftedSubscription(IrcMessage ircMessage) : base(ircMessage) { - private const string AnonymousGifterUserId = "274598607"; - - public List> Badges { get; } - - public List> BadgeInfo { get; } - - public string Color { get; } - - public string DisplayName { get; } - - public string Emotes { get; } - - public string Id { get; } - - public bool IsModerator { get; } - - public bool IsSubscriber { get; } - - public bool IsTurbo { get; } - - public bool IsAnonymous { get; } - - public string Login { get; } - - public string MsgId { get; } - - public string MsgParamMonths { get; } - - public string MsgParamRecipientDisplayName { get; } - - public string MsgParamRecipientId { get; } - - public string MsgParamRecipientUserName { get; } - - public string MsgParamSubPlanName { get; } - - public SubscriptionPlan MsgParamSubPlan { get; } - - public string RoomId { get; } - - public string SystemMsg { get; } - - public string SystemMsgParsed { get; } - - public string TmiSentTs { get; } - - public string UserId { get; } - - public UserType UserType { get; } - - public string MsgParamMultiMonthGiftDuration { get; } + IsAnonymous = UserId == AnonymousGifterUserId; + } - public GiftedSubscription(IrcMessage ircMessage) - { - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - Color = tagValue; - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - Emotes = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.MsgParamMonths: - MsgParamMonths = tagValue; - break; - case Tags.MsgParamRecipientDisplayname: - MsgParamRecipientDisplayName = tagValue; - break; - case Tags.MsgParamRecipientId: - MsgParamRecipientId = tagValue; - break; - case Tags.MsgParamRecipientUsername: - MsgParamRecipientUserName = tagValue; - break; - case Tags.MsgParamSubPlanName: - MsgParamSubPlanName = tagValue; - break; - case Tags.MsgParamSubPlan: - switch (tagValue) - { - case "prime": - MsgParamSubPlan = SubscriptionPlan.Prime; - break; - case "1000": - MsgParamSubPlan = SubscriptionPlan.Tier1; - break; - case "2000": - MsgParamSubPlan = SubscriptionPlan.Tier2; - break; - case "3000": - MsgParamSubPlan = SubscriptionPlan.Tier3; - break; - default: - throw new ArgumentOutOfRangeException(nameof(tagValue.ToLower)); - } - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - IsSubscriber = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.SystemMsg: - SystemMsg = tagValue; - SystemMsgParsed = tagValue.Replace("\\s", " ").Replace("\\n", ""); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.Turbo: - IsTurbo = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.UserId: - UserId = tagValue; - if (UserId == AnonymousGifterUserId) - { - IsAnonymous = true; - } - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - case Tags.MsgParamMultiMonthGiftDuration: - MsgParamMultiMonthGiftDuration = tagValue; - break; - } - } - } + /// + /// Initializes a new instance of the class. + /// + public GiftedSubscription( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + Goal? msgParamGoal, + string msgParamMonths, + string msgParamOriginId, + string msgParamRecipientDisplayName, + string msgParamRecipientId, + string msgParamRecipientUserName, + int msgParamSenderCount, + SubscriptionPlan msgParamSubPlan, + string msgParamSubPlanName, + int msgParamMultiMonthGiftDuration) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamGoal = msgParamGoal; + IsAnonymous = userId == AnonymousGifterUserId; + MsgParamMonths = msgParamMonths; + MsgParamOriginId = msgParamOriginId; + MsgParamRecipientDisplayName = msgParamRecipientDisplayName; + MsgParamRecipientId = msgParamRecipientId; + MsgParamRecipientUserName = msgParamRecipientUserName; + MsgParamSenderCount = msgParamSenderCount; + MsgParamSubPlan = msgParamSubPlan; + MsgParamSubPlanName = msgParamSubPlanName; + MsgParamMultiMonthGiftDuration = msgParamMultiMonthGiftDuration; + } - public GiftedSubscription( - List> badges, - List> badgeInfo, - string color, - string displayName, - string emotes, - string id, - string login, - bool isModerator, - string msgId, - string msgParamMonths, - string msgParamRecipientDisplayName, - string msgParamRecipientId, - string msgParamRecipientUserName, - string msgParamSubPlanName, - string msgMultiMonthDuration, - SubscriptionPlan msgParamSubPlan, - string roomId, - bool isSubscriber, - string systemMsg, - string systemMsgParsed, - string tmiSentTs, - bool isTurbo, - UserType userType, - string userId) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { - Badges = badges; - BadgeInfo = badgeInfo; - Color = color; - DisplayName = displayName; - Emotes = emotes; - Id = id; - Login = login; - IsModerator = isModerator; - MsgId = msgId; - MsgParamMonths = msgParamMonths; - MsgParamRecipientDisplayName = msgParamRecipientDisplayName; - MsgParamRecipientId = msgParamRecipientId; - MsgParamRecipientUserName = msgParamRecipientUserName; - MsgParamSubPlanName = msgParamSubPlanName; - MsgParamSubPlan = msgParamSubPlan; - MsgParamMultiMonthGiftDuration = msgMultiMonthDuration; - RoomId = roomId; - IsSubscriber = isSubscriber; - SystemMsg = systemMsg; - SystemMsgParsed = systemMsgParsed; - TmiSentTs = tmiSentTs; - IsTurbo = isTurbo; - UserType = userType; - UserId = userId; + case Tags.MsgParamMonths: + MsgParamMonths = tag.Value; + break; + case Tags.MsgParamOriginId: + MsgParamOriginId = tag.Value; + break; + case Tags.MsgParamRecipientDisplayName: + MsgParamRecipientDisplayName = tag.Value; + break; + case Tags.MsgParamRecipientId: + MsgParamRecipientId = tag.Value; + break; + case Tags.MsgParamRecipientUsername: + MsgParamRecipientUserName = tag.Value; + break; + case Tags.MsgParamSubPlanName: + MsgParamSubPlanName = tag.Value; + break; + case Tags.MsgParamSubPlan: + MsgParamSubPlan = TagHelper.ToSubscriptionPlan(tag.Value); + break; + case Tags.MsgParamMultiMonthGiftDuration: + MsgParamMultiMonthGiftDuration = int.Parse(tag.Value); + break; + default: + return Goal.TrySetTag(ref _goal, tag); } + return true; } } diff --git a/TwitchLib.Client.Models/Goal.cs b/TwitchLib.Client.Models/Goal.cs new file mode 100644 index 00000000..a86f26f6 --- /dev/null +++ b/TwitchLib.Client.Models/Goal.cs @@ -0,0 +1,41 @@ +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class Goal +{ + public string ContributionType { get; protected set; } = default!; //SUB_POINTS, SUB + + public int CurrentContributions { get; protected set; } + + public string? Description { get; protected set; } + + public int TargetContributions { get; protected set; } + + public int UserContributions { get; protected set; } + + internal static bool TrySetTag(ref Goal? goal, KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamGoalContributionType: + (goal ??= new()).ContributionType = tag.Value; + break; + case Tags.MsgParamGoalCurrentContributions: + (goal ??= new()).CurrentContributions = int.Parse(tag.Value); ; + break; + case Tags.MsgParamGoalDescription: + (goal ??= new()).Description = tag.Value; + break; + case Tags.MsgParamGoalTargetContributions: + (goal ??= new()).TargetContributions = int.Parse(tag.Value); + break; + case Tags.MsgParamGoalUserContributions: + (goal ??= new()).UserContributions = int.Parse(tag.Value); + break; + default: + return false; + } + return true; + } +} diff --git a/TwitchLib.Client.Models/HypeChat.cs b/TwitchLib.Client.Models/HypeChat.cs new file mode 100644 index 00000000..984cbad5 --- /dev/null +++ b/TwitchLib.Client.Models/HypeChat.cs @@ -0,0 +1,65 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +public class HypeChat +{ + /// + /// The value of the Hype Chat sent by the user. + /// + public int Amount { get; internal set; } + + public double CalculatedAmount => Exponent == 0 + ? Amount + : Amount / (10 * Exponent); + + /// + /// The ISO 4217 alphabetic currency code the user has sent the Hype Chat in. + /// + public string Currency { get; internal set; } = default!; + + /// + /// Indicates how many decimal points this currency represents partial amounts in. Decimal points start from the right side of the value defined in pinned-chat-paid-amount. + /// + public int Exponent { get; internal set; } + + /// + /// The level of the Hype Chat, in English.Possible values are: + /// + public PaidLevel Level { get; internal set; } + + /// + /// A Boolean value that determines if the message sent with the Hype Chat was filled in by the system. + /// + /// + /// If true (1), the user entered no message and the body message was automatically filled in by the system. + /// If false (0), the user provided their own message to send with the Hype Chat. + /// + public bool IsSystemMessage { get; internal set; } + + internal static bool TrySetTag(ref HypeChat? hypeChat, KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.PinnedChatPaidAmount: + (hypeChat ??= new()).Amount = int.Parse(tag.Value); + break; + case Tags.PinnedChatPaidCurrency: + (hypeChat ??= new()).Currency = tag.Value; + break; + case Tags.PinnedChatPaidExponent: + (hypeChat ??= new()).Exponent = int.Parse(tag.Value); + break; + case Tags.PinnedChatPaidLevel: + (hypeChat ??= new()).Level = Enum.TryParse(tag.Value, true, out var val) + ? val + : throw new ArgumentException($"Requested value '{tag.Value}' was not found."); + break; + case Tags.PinnedChatPaidIsSystemMessage: + (hypeChat ??= new()).IsSystemMessage = TagHelper.ToBool(tag.Value); + break; + default: + return false; + } + return true; + } +} diff --git a/TwitchLib.Client.Models/Interfaces/IHexColorProperty.cs b/TwitchLib.Client.Models/Interfaces/IHexColorProperty.cs new file mode 100644 index 00000000..91896749 --- /dev/null +++ b/TwitchLib.Client.Models/Interfaces/IHexColorProperty.cs @@ -0,0 +1,9 @@ +namespace TwitchLib.Client.Models.Interfaces; + +public interface IHexColorProperty +{ + /// + /// Property representing HEX color + /// + string HexColor { get; } +} diff --git a/TwitchLib.Client.Models/Interfaces/ISendOptions.cs b/TwitchLib.Client.Models/Interfaces/ISendOptions.cs new file mode 100644 index 00000000..b3a4bff1 --- /dev/null +++ b/TwitchLib.Client.Models/Interfaces/ISendOptions.cs @@ -0,0 +1,33 @@ +using System; + +namespace TwitchLib.Client.Models.Interfaces +{ + public interface ISendOptions + { + /// + /// Number of Messages Allowed Per Instance of the . + /// + uint SendsAllowedInPeriod { get; } + + /// + /// Minimum time between sending items from the queue [in ms] (default 50ms). + /// + ushort SendDelay { get; } + + /// + /// Period Between each reset of the throttling instance window. + /// + TimeSpan ThrottlingPeriod { get; } + + /// + /// The amount of time an object can wait to be sent before it is considered dead, and should be skipped (default 30 minutes). + /// A dead item will be ignored and removed from the send queue when it is hit. + /// + TimeSpan CacheItemTimeout { get; } + + /// + /// Maximum number of Queued outgoing messages (default 10_000). + /// + uint QueueCapacity { get; } + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Internal/IrcMessage.cs b/TwitchLib.Client.Models/Internal/IrcMessage.cs index dd2fcca5..a2e2a3ee 100644 --- a/TwitchLib.Client.Models/Internal/IrcMessage.cs +++ b/TwitchLib.Client.Models/Internal/IrcMessage.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text; using TwitchLib.Client.Enums.Internal; @@ -10,41 +9,44 @@ public class IrcMessage /// /// The channel the message was sent in /// - public string Channel => Params.StartsWith("#") ? Params.Remove(0, 1) : Params; + 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] : ""; /// /// Message itself /// 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] : ""; /// /// Command parameters /// - private readonly string[] _parameters; + private readonly string[]? _parameters; /// /// The user whose message it is /// - public readonly string User; + public string User { get; } /// /// Hostmask of the user /// - public readonly string Hostmask; + public string? Hostmask { get; } /// /// Raw Command /// - public readonly IrcCommand Command; + public IrcCommand Command { get; } /// /// IRCv3 tags /// - public readonly Dictionary Tags; + public Dictionary Tags { get; } + + private string? _rawString; /// /// Create an INCOMPLETE IrcMessage only carrying username @@ -56,7 +58,7 @@ public IrcMessage(string user) User = user; Hostmask = null; Command = IrcCommand.Unknown; - Tags = null; + Tags = new Dictionary(); } /// @@ -70,58 +72,91 @@ public IrcMessage( IrcCommand command, string[] parameters, string hostmask, - Dictionary tags = null) + Dictionary? tags = null) { var idx = hostmask.IndexOf('!'); - User = idx != -1 ? hostmask.Substring(0, idx) : hostmask; + User = idx >= 0 ? hostmask.Substring(0, idx) : hostmask; Hostmask = hostmask; + Command = command; + Tags = tags ?? new Dictionary(); + _parameters = parameters; + if (command == IrcCommand.RPL_353 + && Params.Length > 0 + && Params.Contains("#")) + { + _parameters[0] = $"#{_parameters[0].Split('#')[1]}"; + } + } + + /// + /// Create an IrcMessage, settings its raw string. + /// IrcParser *must* use this constructor, otherwise the raw string + /// will be re-generated each time it is parsed and then passed to handlers. + /// + /// Raw IRC message + /// IRC Command + /// Command params + /// User + /// Hostmask + /// IRCv3 tags + internal IrcMessage( + string raw, + IrcCommand command, + string[] parameters, + string user, + string hostmask, + Dictionary? tags = null) + { + User = user; + Hostmask = hostmask; Command = command; - Tags = tags; + Tags = tags ?? new Dictionary(); - if (command == IrcCommand.RPL_353) + _rawString = raw; + _parameters = parameters; + 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]}"; } } - public new string ToString() + /// + public override string ToString() => _rawString ??= GenerateToString(); + + private string GenerateToString() { - var raw = new StringBuilder(32); - if (Tags != null) + var raw = new StringBuilder(128); + if (Tags?.Count > 0) { - var tags = new string[Tags.Count]; - var i = 0; + raw.Append('@'); foreach (var tag in Tags) { - tags[i] = tag.Key + "=" + tag.Value; - ++i; - } - - if (tags.Length > 0) - { - raw.Append("@").Append(string.Join(";", tags)).Append(" "); + raw.Append(tag.Key).Append('=').Append(tag.Value).Append(';'); } + raw[raw.Length - 1] = ' '; } if (!string.IsNullOrEmpty(Hostmask)) { - raw.Append(":").Append(Hostmask).Append(" "); + raw.Append(':').Append(Hostmask).Append(' '); } - raw.Append(Command.ToString().ToUpper().Replace("RPL_", "")); - if (_parameters.Length <= 0) + // 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]); } @@ -129,4 +164,4 @@ public IrcMessage( return raw.ToString(); } } -} \ No newline at end of file +} diff --git a/TwitchLib.Client.Models/Internal/MsgIds.cs b/TwitchLib.Client.Models/Internal/MsgIds.cs index 531f4f42..ff15d550 100644 --- a/TwitchLib.Client.Models/Internal/MsgIds.cs +++ b/TwitchLib.Client.Models/Internal/MsgIds.cs @@ -1,4 +1,6 @@ -namespace TwitchLib.Client.Models.Internal +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace TwitchLib.Client.Models.Internal { public static class MsgIds { @@ -9,10 +11,13 @@ public static class MsgIds public const string AlreadyR9KOn = "already_r9k_on"; public const string AlreadySubsOff = "already_subs_off"; public const string AlreadySubsOn = "already_subs_on"; + public const string AnonGiftPaidUpgrade = "anongiftpaidupgrade"; public const string Announcement = "announcement"; public const string BadUnbanNoBan = "bad_unban_no_ban"; public const string BanSuccess = "ban_success"; + public const string BitsBadgeTier = "bitsbadgetier"; public const string ColorChanged = "color_changed"; + public const string CommunityPayForward = "communitypayforward"; public const string EmoteOnlyOff = "emote_only_off"; public const string EmoteOnlyOn = "emote_only_on"; public const string HighlightedMessage = "highlighted-message"; @@ -38,16 +43,21 @@ public static class MsgIds public const string RaidErrorSelf = "raid_error_self"; public const string RaidNoticeMature = "raid_notice_mature"; public const string ReSubscription = "resub"; + public const string Ritual = "ritual"; public const string R9KOff = "r9k_off"; public const string R9KOn = "r9k_on"; + public const string StandardPayForward = "standardpayforward"; public const string SubGift = "subgift"; public const string CommunitySubscription = "submysterygift"; public const string ContinuedGiftedSubscription = "giftpaidupgrade"; public const string Subscription = "sub"; public const string SubsOff = "subs_off"; public const string SubsOn = "subs_on"; + public const string SlowOn = "slow_on"; + public const string SlowOff = "slow_off"; public const string TimeoutSuccess = "timeout_success"; public const string UnbanSuccess = "unban_success"; + public const string Unraid = "unraid"; public const string UnrecognizedCmd = "unrecognized_cmd"; public const string UserIntro = "user-intro"; public const string VIPsSuccess = "vips_success"; diff --git a/TwitchLib.Client.Models/Internal/TagHelper.cs b/TwitchLib.Client.Models/Internal/TagHelper.cs new file mode 100644 index 00000000..8a03a187 --- /dev/null +++ b/TwitchLib.Client.Models/Internal/TagHelper.cs @@ -0,0 +1,64 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Extensions; + +namespace TwitchLib.Client.Models.Internal; + +internal static class TagHelper +{ + /// + /// Parses the badges field in GLOBALUSERSTATE, PRIVMSG, USERNOTICE, USERSTATE, etc + /// + /// The data. + /// List of keyvalue pairs representing each badge and value associated + public static List> ToBadges(string badgesStr) + { + var badges = new List>(); + + if (badgesStr.Contains('/')) + { + foreach (var badge in new SpanSliceEnumerator(badgesStr, ',')) + { + var index = badge.IndexOf('/'); + var key = badge.Slice(0, index).ToString(); + var value = badge.Slice(index + 1).ToString(); + badges.Add(new KeyValuePair(key, value)); + } + } + return badges; + } + + public static UserType ToUserType(string s) + { + return s switch + { + "mod" => UserType.Moderator, + "global_mod" => UserType.GlobalModerator, + "admin" => UserType.Admin, + "staff" => UserType.Staff, + _ => UserType.Viewer, + }; + } + + public static SubscriptionPlan ToSubscriptionPlan(string s) + { + return s switch + { + "Prime" or "prime" => SubscriptionPlan.Prime, + "1000" => SubscriptionPlan.Tier1, + "2000" => SubscriptionPlan.Tier2, + "3000" => SubscriptionPlan.Tier3, + _ => throw new ArgumentException($"Unhandled value {s} for {nameof(UserType)}", nameof(s)), + }; + } + + public static bool ToBool(string s) + { + return s == "1"; + } + + public static DateTimeOffset ToDateTimeOffsetFromUnixMs(string s) + { + var timestamp = long.Parse(s); + return DateTimeOffset.FromUnixTimeMilliseconds(timestamp); + } +} diff --git a/TwitchLib.Client.Models/Internal/Tags.cs b/TwitchLib.Client.Models/Internal/Tags.cs index 09978936..4bb8ba86 100644 --- a/TwitchLib.Client.Models/Internal/Tags.cs +++ b/TwitchLib.Client.Models/Internal/Tags.cs @@ -1,4 +1,5 @@ -namespace TwitchLib.Client.Models.Internal +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +namespace TwitchLib.Client.Models.Internal { public static class Tags { @@ -28,7 +29,17 @@ public static class Tags public const string MsgParamDisplayname = "msg-param-displayName"; // Sent only on raid public const string MsgParamLogin = "msg-param-login"; // Sent only on raid public const string MsgParamCumulativeMonths = "msg-param-cumulative-months"; // Sent only on sub, resub + public const string MsgParamGiftTheme = "msg-param-gift-theme"; + public const string MsgParamGoalContributionType = "msg-param-goal-contribution-type"; + public const string MsgParamGoalCurrentContributions = "msg-param-goal-current-contributions"; + public const string MsgParamGoalDescription = "msg-param-goal-description"; + public const string MsgParamGoalTargetContributions = "msg-param-goal-target-contributions"; + public const string MsgParamGoalUserContributions = "msg-param-goal-user-contributions"; public const string MsgParamMonths = "msg-param-months"; // Sent only on subgift, anonsubgift + public const string MsgParamPriorGifterAnonymous = "msg-param-prior-gifter-anonymous"; + public const string MsgParamPriorGifterDisplayName = "msg-param-prior-gifter-display-name"; + public const string MsgParamPriorGifterId = "msg-param-prior-gifter-id"; + public const string MsgParamPriorGifterUserName = "msg-param-prior-gifter-user-name"; public const string MsgParamPromoGiftTotal = "msg-param-promo-gift-total"; // Sent only on anongiftpaidupgrade, giftpaidupgrade public const string MsgParamPromoName = "msg-param-promo-name"; // Sent only on anongiftpaidupgrade, giftpaidupgrade public const string MsgParamShouldShareStreak = "msg-param-should-share-streak"; // Sent only on sub, resub @@ -36,7 +47,8 @@ public static class Tags public const string MsgParamSubPlan = "msg-param-sub-plan"; // Sent only on sub, resub, subgift, anonsubgift public const string MsgParamSubPlanName = "msg-param-sub-plan-name"; // Sent only on sub, resub, subgift, anonsubgift public const string MsgParamViewerCount = "msg-param-viewerCount"; // Sent only on raid - public const string MsgParamRecipientDisplayname = "msg-param-recipient-display-name"; // Sent only on subgift, anonsubgift + public const string MsgParamOriginId = "msg-param-origin-id"; + public const string MsgParamRecipientDisplayName = "msg-param-recipient-display-name"; // Sent only on subgift, anonsubgift public const string MsgParamRecipientId = "msg-param-recipient-id"; // Sent only on subgift, anonsubgift public const string MsgParamRecipientUsername = "msg-param-recipient-user-name"; // Sent only on subgift, anonsubgift public const string MsgParamRitualName = "msg-param-ritual-name"; // Sent only on ritual @@ -46,11 +58,22 @@ public static class Tags public const string MsgParamSenderName = "msg-param-sender-name"; // Sent only on giftpaidupgrade public const string MsgParamThreshold = "msg-param-threshold"; // Sent only on bitsbadgetier public const string Noisy = "noisy"; + public const string PinnedChatPaidAmount = "pinned-chat-paid-amount"; + public const string PinnedChatPaidCurrency = "pinned-chat-paid-currency"; + public const string PinnedChatPaidExponent = "pinned-chat-paid-exponent"; + public const string PinnedChatPaidLevel = "pinned-chat-paid-level"; + public const string PinnedChatPaidIsSystemMessage = "pinned-chat-paid-is-system-message"; + + #region Sent only on replies public const string ReplyParentDisplayName = "reply-parent-display-name"; // Sent only on replies public const string ReplyParentMsgBody = "reply-parent-msg-body"; // Sent only on replies public const string ReplyParentMsgId = "reply-parent-msg-id"; // Sent only on replies public const string ReplyParentUserId = "reply-parent-user-id"; // Sent only on replies public const string ReplyParentUserLogin = "reply-parent-user-login"; // Sent only on replies + public const string ReplyThreadParentMsgId = "reply-thread-parent-msg-id"; + public const string ReplyThreadParentUserLogin = "reply-thread-parent-user-login"; + #endregion + public const string Rituals = "rituals"; public const string RoomId = "room-id"; public const string R9K = "r9k"; diff --git a/TwitchLib.Client.Models/JoinedChannel.cs b/TwitchLib.Client.Models/JoinedChannel.cs index 75df2a63..ecefd752 100644 --- a/TwitchLib.Client.Models/JoinedChannel.cs +++ b/TwitchLib.Client.Models/JoinedChannel.cs @@ -9,11 +9,11 @@ public class JoinedChannel /// The current channel the TwitcChatClient is connected to. public string Channel { get; } - /// Object representing current state of channel (r9k, slow, etc). - public ChannelState ChannelState { get; protected set; } + ///// Object representing current state of channel (r9k, slow, etc). + //public ChannelState ChannelState { get; protected set; } /// The most recent message received. - public ChatMessage PreviousMessage { get; protected set; } + public ChatMessage? PreviousMessage { get; protected set; } /// JoinedChannel object constructor. public JoinedChannel(string channel) diff --git a/TwitchLib.Client.Models/MessageEmote.cs b/TwitchLib.Client.Models/MessageEmote.cs index 72e70d14..73e458b2 100644 --- a/TwitchLib.Client.Models/MessageEmote.cs +++ b/TwitchLib.Client.Models/MessageEmote.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; +using System.Text; using System.Text.RegularExpressions; // TODO: Missing builder @@ -73,6 +73,23 @@ public class MessageEmote "//cdn.betterttv.net/emote/{0}/3x" } ); + + /// + /// Collection of Composite Format Strings which will substitute + /// an emote ID to get a URL for an image from the 7tv CDN + /// + /// + /// These are sorted such that the enum can be used as an index, + /// eg SevenTvEmoteUrls[] + /// + public static readonly ReadOnlyCollection SevenTvEmoteUrls = new ReadOnlyCollection( + new[] + { + "//cdn.7tv.app/emote/{0}/1x.avif", + "//cdn.7tv.app/emote/{0}/2x.avif", + "//cdn.7tv.app/emote/{0}/5x.avif" + } + ); #endregion Third-Party Emote URLs /// @@ -85,16 +102,14 @@ public class MessageEmote public static string SourceMatchingReplacementText(MessageEmote caller) { var sizeIndex = (int)caller.Size; - switch (caller.Source) + return caller.Source switch { - case EmoteSource.BetterTwitchTv: - return string.Format(BetterTwitchTvEmoteUrls[sizeIndex], caller.Id); - case EmoteSource.FrankerFaceZ: - return string.Format(FrankerFaceZEmoteUrls[sizeIndex], caller.Id); - case EmoteSource.Twitch: - return string.Format(TwitchEmoteUrls[sizeIndex], caller.Id); - } - return caller.Text; + EmoteSource.SevenTv => string.Format(SevenTvEmoteUrls[sizeIndex], caller.Id), + EmoteSource.BetterTwitchTv => string.Format(BetterTwitchTvEmoteUrls[sizeIndex], caller.Id), + EmoteSource.FrankerFaceZ => string.Format(FrankerFaceZEmoteUrls[sizeIndex], caller.Id), + EmoteSource.Twitch => string.Format(TwitchEmoteUrls[sizeIndex], caller.Id), + _ => caller.Text, + }; } /// Enum supplying the supported sites which provide Emote images. @@ -107,7 +122,10 @@ public enum EmoteSource FrankerFaceZ, /// Emotes hosted by BetterTTV.net - BetterTwitchTv + BetterTwitchTv, + + /// Emotes hosted by 7tv + SevenTv } /// Enum denoting the emote sizes @@ -132,30 +150,26 @@ public enum EmoteSize Large = 2 } - private readonly string _id, _text, _escapedText; - private readonly EmoteSource _source; - private readonly EmoteSize _size; - /// /// Emote ID as used by the emote source. Will be provided as {0} /// to be substituted into the indicated URL if needed. /// - public string Id => _id; + public string Id { get; } /// /// Emote text which appears in a message and is meant to be replaced by the emote image. /// - public string Text => _text; + public string Text { get; } /// /// The specified for this emote. /// - public EmoteSource Source => _source; + public EmoteSource Source { get; } /// /// The specified for this emote. /// - public EmoteSize Size => _size; + public EmoteSize Size { get; } /// /// The string to substitute emote text for. @@ -172,7 +186,7 @@ public enum EmoteSize /// The emote text regex-escaped /// so that it can be embedded into a regex pattern. /// - public string EscapedText => _escapedText; + public string EscapedText { get; } /// /// Constructor for a new MessageEmote instance. @@ -201,13 +215,13 @@ public MessageEmote( string text, EmoteSource source = EmoteSource.Twitch, EmoteSize size = EmoteSize.Small, - ReplaceEmoteDelegate replacementDelegate = null) + ReplaceEmoteDelegate? replacementDelegate = null) { - _id = id; - _text = text; - _escapedText = Regex.Escape(text); - _source = source; - _size = size; + Id = id; + Text = text; + EscapedText = Regex.Escape(text); + Source = source; + Size = size; if (replacementDelegate != null) { ReplacementDelegate = replacementDelegate; @@ -220,30 +234,36 @@ public MessageEmote( /// public class MessageEmoteCollection { - private readonly SortedList _emoteList; - private const string BasePattern = @"(\b{0}\b)"; + private readonly Dictionary _emotes; +#if NET8_0_OR_GREATER + private static readonly CompositeFormat BasePattern = CompositeFormat.Parse(@"(\b {0}\b)|(\b{0} \b)|(?<=\W){0}(?=$)|(?<=\s){0}(?=\s)|(^{0}$)"); +#else + private const string BasePattern = @"(\b {0}\b)|(\b{0} \b)|(?<=\W){0}(?=$)|(?<=\s){0}(?=\s)|(^{0}$)"; +#endif /// Do not access directly! Backing field for - private string _currentPattern; - private Regex _regex; + private string? _currentPattern; + private Regex? _regex; private readonly EmoteFilterDelegate _preferredFilter; /// /// Property so that we can be confident /// always reflects changes to . /// - private string CurrentPattern + private string? CurrentPattern { get => _currentPattern; set { - if (_currentPattern != null && _currentPattern.Equals(value)) return; + if (_currentPattern?.Equals(value) is true) + return; + _currentPattern = value; PatternChanged = true; } } - private Regex CurrentRegex + private Regex? CurrentRegex { get { @@ -273,7 +293,7 @@ private Regex CurrentRegex /// public MessageEmoteCollection() { - _emoteList = new SortedList(); + _emotes = new(); _preferredFilter = AllInclusiveEmoteFilter; } @@ -294,19 +314,23 @@ public MessageEmoteCollection(EmoteFilterDelegate preferredFilter) : this() /// The to add to the collection. public void Add(MessageEmote emote) { - if (!_emoteList.TryGetValue(emote.Text, out var _)) - { - _emoteList.Add(emote.Text, emote); - } +#if NETSTANDARD2_0 + if (_emotes.ContainsKey(emote.Text)) + return; + _emotes.Add(emote.Text, emote); +#else + if(!_emotes.TryAdd(emote.Text, emote)) + return; +#endif if (CurrentPattern == null) { //string i = String.Format(_basePattern, "(" + emote.EscapedText + "){0}"); - CurrentPattern = string.Format(BasePattern, emote.EscapedText); + CurrentPattern = string.Format(null, BasePattern, emote.EscapedText); } else { - CurrentPattern = CurrentPattern + "|" + string.Format(BasePattern, emote.EscapedText); + CurrentPattern = CurrentPattern + "|" + string.Format(null, BasePattern, emote.EscapedText); } } @@ -333,9 +357,8 @@ public void Merge(IEnumerable emotes) /// The to remove. public void Remove(MessageEmote emote) { - if (!_emoteList.ContainsKey(emote.Text)) return; - - _emoteList.Remove(emote.Text); + if (!_emotes.Remove(emote.Text)) + return; // These patterns look a lot scarier than they are because we have to look for // a lot of regex characters, which means we do a lot of escaping! @@ -346,7 +369,7 @@ public void Remove(MessageEmote emote) // Matches |(\bEMOTE\b) including the preceding | so that the following | and emote (if any) // merge seamlessly when this section is removed. Again, wrapped in a group. var otherEmotePattern = @"(\|\(\\b" + emote.EscapedText + @"\\b\))"; - var newPattern = Regex.Replace(CurrentPattern, firstEmotePattern + "|" + otherEmotePattern, ""); + var newPattern = Regex.Replace(CurrentPattern, firstEmotePattern + "|" + otherEmotePattern, ""); // todo: possible ArgumentNullException CurrentPattern = newPattern.Equals("") ? null : newPattern; } @@ -355,7 +378,7 @@ public void Remove(MessageEmote emote) /// public void RemoveAll() { - _emoteList.Clear(); + _emotes.Clear(); CurrentPattern = null; } @@ -372,17 +395,48 @@ public void RemoveAll() /// received is to be replaced. /// Defaults to . /// + /// + /// String providing additional detection capabilities for further processing + /// + /// + /// String providing additional detection capabilities for further processing + /// /// /// A string where all of the original emote text has been replaced with /// its designated s /// - public string ReplaceEmotes(string originalMessage, EmoteFilterDelegate del = null) + public string ReplaceEmotes(string originalMessage, EmoteFilterDelegate? del = null, string prefix = "", string suffix = "") { - if (CurrentRegex == null) return originalMessage; - if (del != null && del != CurrentEmoteFilter) CurrentEmoteFilter = del; - var newMessage = CurrentRegex.Replace(originalMessage, GetReplacementString); + if (CurrentRegex == null) + { + return originalMessage; + } + + if (del != null && del != CurrentEmoteFilter) + { + CurrentEmoteFilter = del; + } + + var newMessage = CurrentRegex.Replace(originalMessage, match => + { + // the match includes possible white space on either side, so we need to preserve that + var emoteCode = match.Value.Trim(); + if (match.Value[0] == ' ') + prefix += " "; + if (match.Value[match.Value.Length - 1] == ' ') + suffix = " " + suffix; + if (!_emotes.TryGetValue(emoteCode, out var emote)) + { + return match.Value; + } + + return CurrentEmoteFilter(emote) ? prefix + emote.ReplacementString + suffix : match.Value; + }); CurrentEmoteFilter = _preferredFilter; - return newMessage; + + // the hacky replacement logic above will leave 2 spaces between emote and non-emote + // twitch doesn't allow this anyways, so this fix should be fine + return newMessage.Replace(" ", " "); } /// @@ -415,14 +469,5 @@ public static bool TwitchOnlyEmoteFilter(MessageEmote emote) { return emote.Source == MessageEmote.EmoteSource.Twitch; } - - private string GetReplacementString(Match m) - { - if (!_emoteList.ContainsKey(m.Value)) return m.Value; - - var emote = _emoteList[m.Value]; - return CurrentEmoteFilter(emote) ? emote.ReplacementString : m.Value; - //If the match doesn't exist in the list ("shouldn't happen") or the filter excludes it, don't replace. - } } } diff --git a/TwitchLib.Client.Models/OutboundChatMessage.cs b/TwitchLib.Client.Models/OutboundChatMessage.cs index 6efc8a6c..3e40ec77 100644 --- a/TwitchLib.Client.Models/OutboundChatMessage.cs +++ b/TwitchLib.Client.Models/OutboundChatMessage.cs @@ -6,22 +6,24 @@ public class OutboundChatMessage public string Message { get; set; } - public string Username { get; set; } - - public string ReplyToId { get; set; } + public string? ReplyToId { get; set; } + /// + /// Initializes a new instance of the class. + /// + public OutboundChatMessage(string channel, string message) + { + Channel = channel; + Message = message; + } + + /// public override string ToString() { - var user = Username.ToLower(); var channel = Channel.ToLower(); - if(ReplyToId == null) - { - return $":{user}!{user}@{user}.tmi.twitch.tv PRIVMSG #{channel} :{Message}"; - } else - { - return $"@reply-parent-msg-id={ReplyToId} PRIVMSG #{channel} :{Message}"; - } - + return ReplyToId is null + ? $"PRIVMSG #{channel} :{Message}" + : $"@reply-parent-msg-id={ReplyToId} PRIVMSG #{channel} :{Message}"; } } } diff --git a/TwitchLib.Client.Models/OutboundWhisperMessage.cs b/TwitchLib.Client.Models/OutboundWhisperMessage.cs deleted file mode 100644 index 35d9d00c..00000000 --- a/TwitchLib.Client.Models/OutboundWhisperMessage.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace TwitchLib.Client.Models -{ - public class OutboundWhisperMessage - { - public string Username { get; set; } - - public string Receiver { get; set; } - - public string Message { get; set; } - - public override string ToString() - { - return $":{Username}~{Username}@{Username}.tmi.twitch.tv PRIVMSG #jtv :/w {Receiver} {Message}"; - } - } -} diff --git a/TwitchLib.Client.Models/OutgoingMessage.cs b/TwitchLib.Client.Models/OutgoingMessage.cs deleted file mode 100644 index e5aa69ba..00000000 --- a/TwitchLib.Client.Models/OutgoingMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace TwitchLib.Client.Models -{ - public class OutgoingMessage - { - public string Channel { get; set; } - - public string Message { get; set; } - - public int Nonce { get; set; } - - public string Sender { get; set; } - - public MessageState State { get; set; } - } - - public enum MessageState : byte - { - /// Message did not originate from this session, or was successfully sent. - Normal = 0, - - /// Message is current queued. - Queued, - - /// Message failed to be sent. - Failed - } -} - diff --git a/TwitchLib.Client.Models/PrimePaidSubscriber.cs b/TwitchLib.Client.Models/PrimePaidSubscriber.cs index 81ed1e9d..53c56fa5 100644 --- a/TwitchLib.Client.Models/PrimePaidSubscriber.cs +++ b/TwitchLib.Client.Models/PrimePaidSubscriber.cs @@ -1,76 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Text; -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +public class PrimePaidSubscriber : UserNoticeBase { - public class PrimePaidSubscriber : SubscriberBase + /// + /// The type of subscription plan being used. + /// + public SubscriptionPlan MsgParamSubPlan { get; protected set; } + + /// + /// Property representing system message. + /// + public string ResubMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The IRC message from Twitch to be processed. + public PrimePaidSubscriber(IrcMessage ircMessage) : base(ircMessage) { - public PrimePaidSubscriber(IrcMessage ircMessage) : base(ircMessage) - { - } + ResubMessage = ircMessage.Message; + } - public PrimePaidSubscriber( - List> badges, - List> badgeInfo, - string colorHex, - Color color, - string displayName, - string emoteSet, - string id, - string login, - string systemMessage, - string msgId, - string msgParamCumulativeMonths, - string msgParamStreakMonths, - bool msgParamShouldShareStreak, - string systemMessageParsed, - string resubMessage, - SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, - string roomId, - string userId, - bool isModerator, - bool isTurbo, - bool isSubscriber, - bool isPartner, - string tmiSentTs, - UserType userType, - string rawIrc, - string channel, - int months = 0) - : base(badges, - badgeInfo, - colorHex, - color, - displayName, - emoteSet, - id, - login, - systemMessage, - msgId, - msgParamCumulativeMonths, - msgParamStreakMonths, - msgParamShouldShareStreak, - systemMessageParsed, - resubMessage, - subscriptionPlan, - subscriptionPlanName, - roomId, - userId, - isModerator, - isTurbo, - isSubscriber, - isPartner, - tmiSentTs, - userType, - rawIrc, - channel, - months) + /// + /// Initializes a new instance of the class. + /// + public PrimePaidSubscriber( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + SubscriptionPlan msgParamSubPlan, + string resubMessage) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamSubPlan = msgParamSubPlan; + ResubMessage = resubMessage; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { + case Tags.MsgParamSubPlan: + MsgParamSubPlan = TagHelper.ToSubscriptionPlan(tag.Value); + break; + default: + return false; } + return true; } } diff --git a/TwitchLib.Client.Models/RaidNotification.cs b/TwitchLib.Client.Models/RaidNotification.cs index 3db1f1bf..d1a24921 100644 --- a/TwitchLib.Client.Models/RaidNotification.cs +++ b/TwitchLib.Client.Models/RaidNotification.cs @@ -1,180 +1,92 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +public class RaidNotification : UserNoticeBase { - public class RaidNotification + /// + /// The display name of the broadcaster raiding this channel. + /// + public string MsgParamDisplayName { get; protected set; } = default!; + + /// + /// The login name of the broadcaster raiding this channel. + /// + public string MsgParamLogin { get; protected set; } = default!; + + /// + /// The number of viewers raiding this channel from the broadcaster’s channel. + /// + public string MsgParamViewerCount { get; protected set; } = default!; + + /// + /// Initializes a new instance of the class. + /// + public RaidNotification(IrcMessage ircMessage) : base(ircMessage) { - public List> Badges { get; } - - public List> BadgeInfo { get; } - - public string Color { get; } - - public string DisplayName { get; } - - public string Emotes { get; } - - public string Id { get; } - - public string Login { get; } - - public bool Moderator { get; } - - public string MsgId { get; } - - public string MsgParamDisplayName { get; } - - public string MsgParamLogin { get; } - - public string MsgParamViewerCount { get; } - - public string RoomId { get; } - - public bool Subscriber { get; } - - public string SystemMsg { get; } - - public string SystemMsgParsed { get; } - - public string TmiSentTs { get; } - - public bool Turbo { get; } - - public string UserId { get; } - - public UserType UserType { get; } - - // @badges=;color=#FF0000;display-name=Heinki;emotes=;id=4fb7ab2d-aa2c-4886-a286-46e20443f3d6;login=heinki;mod=0;msg-id=raid;msg-param-displayName=Heinki;msg-param-login=heinki;msg-param-viewerCount=4;room-id=27229958;subscriber=0;system-msg=4\sraiders\sfrom\sHeinki\shave\sjoined\n!;tmi-sent-ts=1510249711023;turbo=0;user-id=44110799;user-type= :tmi.twitch.tv USERNOTICE #pandablack - public RaidNotification(IrcMessage ircMessage) - { - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; + } - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - Color = tagValue; - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - Emotes = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - Moderator = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.MsgParamDisplayname: - MsgParamDisplayName = tagValue; - break; - case Tags.MsgParamLogin: - MsgParamLogin = tagValue; - break; - case Tags.MsgParamViewerCount: - MsgParamViewerCount = tagValue; - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - Subscriber = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.SystemMsg: - SystemMsg = tagValue; - SystemMsgParsed = tagValue.Replace("\\s", " ").Replace("\\n", ""); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.Turbo: - Turbo = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.UserId: - UserId = tagValue; - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - } - } - } + /// + /// Initializes a new instance of the class. + /// + public RaidNotification( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + string msgParamDisplayName, + string msgParamLogin, + string msgParamViewerCount) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamDisplayName = msgParamDisplayName; + MsgParamLogin = msgParamLogin; + MsgParamViewerCount = msgParamViewerCount; + } - public RaidNotification( - List> badges, - List> badgeInfo, - string color, - string displayName, - string emotes, - string id, - string login, - bool moderator, - string msgId, - string msgParamDisplayName, - string msgParamLogin, - string msgParamViewerCount, - string roomId, - bool subscriber, - string systemMsg, - string systemMsgParsed, - string tmiSentTs, - bool turbo, - UserType userType, - string userId) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { - Badges = badges; - BadgeInfo = badgeInfo; - Color = color; - DisplayName = displayName; - Emotes = emotes; - Id = id; - Login = login; - Moderator = moderator; - MsgId = msgId; - MsgParamDisplayName = msgParamDisplayName; - MsgParamLogin = msgParamLogin; - MsgParamViewerCount = msgParamViewerCount; - RoomId = roomId; - Subscriber = subscriber; - SystemMsg = systemMsg; - SystemMsgParsed = systemMsgParsed; - TmiSentTs = tmiSentTs; - Turbo = turbo; - UserType = userType; - UserId = userId; + case Tags.MsgParamDisplayname: + MsgParamDisplayName = tag.Value; + break; + case Tags.MsgParamLogin: + MsgParamLogin = tag.Value; + break; + case Tags.MsgParamViewerCount: + MsgParamViewerCount = tag.Value; + break; + default: + return false; } + return true; } } - diff --git a/TwitchLib.Client.Models/ReSubscriber.cs b/TwitchLib.Client.Models/ReSubscriber.cs index e7b56053..b93ab76f 100644 --- a/TwitchLib.Client.Models/ReSubscriber.cs +++ b/TwitchLib.Client.Models/ReSubscriber.cs @@ -1,77 +1,117 @@ -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +public class ReSubscriber : UserNoticeBase { - public class ReSubscriber : SubscriberBase + /// + /// The total number of months the user has subscribed. + /// + public int MsgParamCumulativeMonths { get; protected set; } + + /// + /// A Boolean value that indicates whether the user wants their streaks shared. + /// + public bool MsgParamShouldShareStreak { get; protected set; } + + /// + /// The number of consecutive months the user has subscribed. + /// + public int MsgParamStreakMonths { get; protected set; } + + /// + /// The type of subscription plan being used. + /// + public SubscriptionPlan MsgParamSubPlan { get; protected set; } + + /// + /// The display name of the subscription plan. This may be a default name or one created by the channel owner. + /// + public string MsgParamSubPlanName { get; protected set; } = default!; + + public string ResubMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public ReSubscriber(IrcMessage ircMessage) : base(ircMessage) { - public int Months => monthsInternal; + ResubMessage = ircMessage.Message; + } - public ReSubscriber(IrcMessage ircMessage) : base(ircMessage) - { - } + /// + /// Initializes a new instance of the class. + /// + public ReSubscriber( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + int msgParamCumulativeMonths, + bool msgParamShouldShareStreak, + int msgParamStreakMonths, + SubscriptionPlan msgParamSubPlan, + string msgParamSubPlanName, + string resubMessage) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamCumulativeMonths = msgParamCumulativeMonths; + MsgParamShouldShareStreak = msgParamShouldShareStreak; + MsgParamStreakMonths = msgParamStreakMonths; + MsgParamSubPlan = msgParamSubPlan; + MsgParamSubPlanName = msgParamSubPlanName; + ResubMessage = resubMessage; + } - public ReSubscriber( - List> badges, - List> badgeInfo, - string colorHex, - Color color, - string displayName, - string emoteSet, - string id, - string login, - string systemMessage, - string msgId, - string msgParamCumulativeMonths, - string msgParamStreakMonths, - bool msgParamShouldShareStreak, - string systemMessageParsed, - string resubMessage, - SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, - string roomId, - string userId, - bool isModerator, - bool isTurbo, - bool isSubscriber, - bool isPartner, - string tmiSentTs, - UserType userType, - string rawIrc, - string channel, - int months = 0) - : base(badges, - badgeInfo, - colorHex, - color, - displayName, - emoteSet, - id, - login, - systemMessage, - msgId, - msgParamCumulativeMonths, - msgParamStreakMonths, - msgParamShouldShareStreak, - systemMessageParsed, - resubMessage, - subscriptionPlan, - subscriptionPlanName, - roomId, - userId, - isModerator, - isTurbo, - isSubscriber, - isPartner, - tmiSentTs, - userType, - rawIrc, - channel, - months) + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { + case Tags.MsgParamCumulativeMonths: + MsgParamCumulativeMonths = int.Parse(tag.Value); + break; + case Tags.MsgParamShouldShareStreak: + MsgParamShouldShareStreak = TagHelper.ToBool(tag.Value); + break; + case Tags.MsgParamStreakMonths: + MsgParamStreakMonths = int.Parse(tag.Value); + break; + case Tags.MsgParamSubPlan: + MsgParamSubPlan = TagHelper.ToSubscriptionPlan(tag.Value); + break; + case Tags.MsgParamSubPlanName: + MsgParamSubPlanName = tag.Value; + break; + default: + return false; } + return true; } } diff --git a/TwitchLib.Client.Models/Ritual.cs b/TwitchLib.Client.Models/Ritual.cs new file mode 100644 index 00000000..59810f01 --- /dev/null +++ b/TwitchLib.Client.Models/Ritual.cs @@ -0,0 +1,77 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class Ritual : UserNoticeBase +{ + /// + /// The name of the ritual being celebrated. + /// + public string MsgParamRitualName { get; protected set; } = default!; + + public string Message { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public Ritual(IrcMessage ircMessage) : base(ircMessage) + { + Message = ircMessage.Message; + } + + /// + /// Initializes a new instance of the class. + /// + public Ritual( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + string msgParamRitualName, + string message) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamRitualName = msgParamRitualName; + Message = message; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamRitualName: + MsgParamRitualName = tag.Value; + break; + default: + return false; + } + return true; + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/RitualNewChatter.cs b/TwitchLib.Client.Models/RitualNewChatter.cs deleted file mode 100644 index 4fc688a6..00000000 --- a/TwitchLib.Client.Models/RitualNewChatter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Collections.Generic; - -using TwitchLib.Client.Enums; -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Models -{ - public class RitualNewChatter - { - public List> Badges { get; } - - public List> BadgeInfo { get; } - - public string Color { get; } - - public string DisplayName { get; } - - public string Emotes { get; } - - public string Id { get; } - - public bool IsModerator { get; } - - public bool IsSubscriber { get; } - - public bool IsTurbo { get; } - - public string Login { get; } - - public string Message { get; } - - public string MsgId { get; } - - public string MsgParamRitualName { get; } - - public string RoomId { get; } - - public string SystemMsgParsed { get; } - - public string SystemMsg { get; } - - public string TmiSentTs { get; } - - public string UserId { get; } - - public UserType UserType { get; } - - // 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 - public RitualNewChatter(IrcMessage ircMessage) - { - Message = ircMessage.Message; - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - Color = tagValue; - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - Emotes = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.MsgParamRitualName: - MsgParamRitualName = tagValue; - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - IsSubscriber = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.SystemMsg: - SystemMsg = tagValue; - SystemMsgParsed = tagValue.Replace("\\s", " ").Replace("\\n", ""); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.Turbo: - IsTurbo = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.UserId: - UserId = tagValue; - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - } - } - } - } -} diff --git a/TwitchLib.Client.Models/SendOptions.cs b/TwitchLib.Client.Models/SendOptions.cs new file mode 100644 index 00000000..d31e4e1c --- /dev/null +++ b/TwitchLib.Client.Models/SendOptions.cs @@ -0,0 +1,55 @@ +using System; +using TwitchLib.Client.Models.Interfaces; + +namespace TwitchLib.Client.Models +{ + public class SendOptions : ISendOptions + { + /// + public uint SendsAllowedInPeriod { get; } + + /// + public ushort SendDelay { get; } + + /// + public TimeSpan ThrottlingPeriod { get; } = TimeSpan.FromSeconds(30); + + /// + public uint QueueCapacity { get; } + + /// + public TimeSpan CacheItemTimeout { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A of zero means: + ///

+ /// all messages that are enqueued to send + /// are going to be throttled! + ///

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SendOptions( + uint sendsAllowedInPeriod = 20, + uint queueCapacity = 10_000, + uint cacheItemTimeoutInMinutes = 30, + ushort sendDelay = 50) + { + SendsAllowedInPeriod = sendsAllowedInPeriod; + QueueCapacity = queueCapacity; + CacheItemTimeout = TimeSpan.FromMinutes(cacheItemTimeoutInMinutes); + SendDelay = sendDelay; + } + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/SentMessage.cs b/TwitchLib.Client.Models/SentMessage.cs index 764246f1..7b083a82 100644 --- a/TwitchLib.Client.Models/SentMessage.cs +++ b/TwitchLib.Client.Models/SentMessage.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Interfaces; namespace TwitchLib.Client.Models { /// Model representing a sent message. - public class SentMessage + public class SentMessage : IHexColorProperty { /// Badges the sender has public List> Badges { get; } @@ -12,7 +13,7 @@ public class SentMessage public string Channel { get; } /// Sender's name color. - public string ColorHex { get; } + public string HexColor { get; } /// Display name of the sender. public string DisplayName { get; } @@ -30,7 +31,7 @@ public class SentMessage public string Message { get; } /// The type of user (admin, broadcaster, viewer, moderator) - public Enums.UserType UserType { get; } + public UserType UserType { get; } /// Model constructor. public SentMessage( @@ -39,7 +40,7 @@ public SentMessage( { Badges = state.Badges; Channel = state.Channel; - ColorHex = state.ColorHex; + HexColor = state.HexColor; DisplayName = state.DisplayName; EmoteSet = state.EmoteSet; IsModerator = state.IsModerator; @@ -48,20 +49,23 @@ public SentMessage( Message = message; } + /// + /// Initializes a new instance of the class. + /// public SentMessage( List> badges, string channel, - string colorHex, + string hexColor, string displayName, string emoteSet, bool isModerator, bool isSubscriber, - Enums.UserType userType, + UserType userType, string message) { Badges = badges; Channel = channel; - ColorHex = colorHex; + HexColor = hexColor; DisplayName = displayName; EmoteSet = emoteSet; IsModerator = isModerator; diff --git a/TwitchLib.Client.Models/StandardPayForward.cs b/TwitchLib.Client.Models/StandardPayForward.cs new file mode 100644 index 00000000..23bbc696 --- /dev/null +++ b/TwitchLib.Client.Models/StandardPayForward.cs @@ -0,0 +1,111 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class StandardPayForward : UserNoticeBase +{ + public bool MsgParamPriorGifterAnonymous { get; protected set; } + + public string MsgParamPriorGifterDisplayName { get; protected set; } = default!; + + public long MsgParamPriorGifterId { get; protected set; } + + public string MsgParamPriorGifterUserName { get; protected set; } = default!; + + public string? MsgParamRecipientDisplayName { get; protected set; } + + public long? MsgParamRecipientId { get; protected set; } + + public string? MsgParamRecipientUserName { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + public StandardPayForward(IrcMessage ircMessage) : base(ircMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public StandardPayForward( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + bool msgParamPriorGifterAnonymous, + string msgParamPriorGifterDisplayName, + long msgParamPriorGifterId, + string msgParamPriorGifterUserName, + string? msgParamRecipientDisplayName, + long? msgParamRecipientId, + string? msgParamRecipientUserName) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamPriorGifterAnonymous = msgParamPriorGifterAnonymous; + MsgParamPriorGifterDisplayName = msgParamPriorGifterDisplayName; + MsgParamPriorGifterId = msgParamPriorGifterId; + MsgParamPriorGifterUserName = msgParamPriorGifterUserName; + MsgParamRecipientDisplayName = msgParamRecipientDisplayName; + MsgParamRecipientId = msgParamRecipientId; + MsgParamRecipientUserName = msgParamRecipientUserName; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) + { + case Tags.MsgParamPriorGifterAnonymous: + MsgParamPriorGifterAnonymous = bool.Parse(tag.Value); + break; + case Tags.MsgParamPriorGifterDisplayName: + MsgParamPriorGifterDisplayName = tag.Value; + break; + case Tags.MsgParamPriorGifterId: + MsgParamPriorGifterId = long.Parse(tag.Value); + break; + case Tags.MsgParamPriorGifterUserName: + MsgParamPriorGifterUserName = tag.Value; + break; + case Tags.MsgParamRecipientDisplayName: + MsgParamRecipientDisplayName = tag.Value; + break; + case Tags.MsgParamRecipientId: + MsgParamRecipientId = long.Parse(tag.Value); + break; + case Tags.MsgParamRecipientUsername: + MsgParamRecipientUserName = tag.Value; + break; + default: + return false; + } + return true; + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/Subscriber.cs b/TwitchLib.Client.Models/Subscriber.cs index 116a6e87..d5fd3964 100644 --- a/TwitchLib.Client.Models/Subscriber.cs +++ b/TwitchLib.Client.Models/Subscriber.cs @@ -1,75 +1,117 @@ -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; using TwitchLib.Client.Models.Internal; -namespace TwitchLib.Client.Models +namespace TwitchLib.Client.Models; + +public class Subscriber : UserNoticeBase { - public class Subscriber : SubscriberBase + /// + /// The total number of months the user has subscribed. + /// + public int MsgParamCumulativeMonths { get; protected set; } + + /// + /// A Boolean value that indicates whether the user wants their streaks shared. + /// + public bool MsgParamShouldShareStreak { get; protected set; } + + /// + /// The number of consecutive months the user has subscribed. + /// + public int MsgParamStreakMonths { get; protected set; } + + /// + /// The type of subscription plan being used. + /// + public SubscriptionPlan MsgParamSubPlan { get; protected set; } + + /// + /// The display name of the subscription plan. This may be a default name or one created by the channel owner. + /// + public string MsgParamSubPlanName { get; protected set; } = default!; + + public string ResubMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public Subscriber(IrcMessage ircMessage) : base(ircMessage) { - public Subscriber(IrcMessage ircMessage) - : base(ircMessage) - { - } + ResubMessage = ircMessage.Message; + } - public Subscriber( - List> badges, - List> badgeInfo, - string colorHex, - Color color, - string displayName, - string emoteSet, - string id, - string login, - string systemMessage, - string msgId, - string msgParamCumulativeMonths, - string msgParamStreakMonths, - bool msgParamShouldShareStreak, - string systemMessageParsed, - string resubMessage, - SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, - string roomId, - string userId, - bool isModerator, - bool isTurbo, - bool isSubscriber, - bool isPartner, - string tmiSentTs, - UserType userType, - string rawIrc, - string channel) - : base(badges, - badgeInfo, - colorHex, - color, - displayName, - emoteSet, - id, - login, - systemMessage, - msgId, - msgParamCumulativeMonths, - msgParamStreakMonths, - msgParamShouldShareStreak, - systemMessageParsed, - resubMessage, - subscriptionPlan, - subscriptionPlanName, - roomId, - userId, - isModerator, - isTurbo, - isSubscriber, - isPartner, - tmiSentTs, - userType, - rawIrc, - channel, - months: 0) + /// + /// Initializes a new instance of the class. + /// + public Subscriber( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags, + int msgParamCumulativeMonths, + bool msgParamShouldShareStreak, + int msgParamStreakMonths, + SubscriptionPlan msgParamSubPlan, + string msgParamSubPlanName, + string resubMessage) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + MsgParamCumulativeMonths = msgParamCumulativeMonths; + MsgParamShouldShareStreak = msgParamShouldShareStreak; + MsgParamStreakMonths = msgParamStreakMonths; + MsgParamSubPlan = msgParamSubPlan; + MsgParamSubPlanName = msgParamSubPlanName; + ResubMessage = resubMessage; + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + switch (tag.Key) { + case Tags.MsgParamCumulativeMonths: + MsgParamCumulativeMonths = int.Parse(tag.Value); + break; + case Tags.MsgParamShouldShareStreak: + MsgParamShouldShareStreak = TagHelper.ToBool(tag.Value); + break; + case Tags.MsgParamStreakMonths: + MsgParamStreakMonths = int.Parse(tag.Value); + break; + case Tags.MsgParamSubPlan: + MsgParamSubPlan = TagHelper.ToSubscriptionPlan(tag.Value); + break; + case Tags.MsgParamSubPlanName: + MsgParamSubPlanName = tag.Value; + break; + default: + return false; } + return true; } } diff --git a/TwitchLib.Client.Models/SubscriberBase.cs b/TwitchLib.Client.Models/SubscriberBase.cs deleted file mode 100644 index 03c3cdc0..00000000 --- a/TwitchLib.Client.Models/SubscriberBase.cs +++ /dev/null @@ -1,288 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; -using TwitchLib.Client.Models.Extensions.NetCore; -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Models -{ - /// Class representing a resubscriber. - public class SubscriberBase - { - /// Property representing list of badges assigned. - public List> Badges { get; } - - /// Metadata associated with each badge - public List> BadgeInfo { get; } - - /// Property representing the colorhex of the resubscriber. - public string ColorHex { get; } - - /// Property representing HEX color as a System.Drawing.Color object. - public Color Color { get; } - - /// Property representing resubscriber's customized display name. - public string DisplayName { get; } - - /// Property representing emote set of resubscriber. - public string EmoteSet { get; } - - /// Property representing resub message id - public string Id { get; } - - /// Property representing whether or not the resubscriber is a moderator. - public bool IsModerator { get; } - - /// Property representing whether or not person is a partner. - public bool IsPartner { get; } - - /// Property representing whether or not the resubscriber is a subscriber (YES). - public bool IsSubscriber { get; } - - /// Property representing whether or not the resubscriber is a turbo member. - public bool IsTurbo { get; } - - /// Property representing login of resubscription event. - public string Login { get; } - - public string MsgId { get; } - - public string MsgParamCumulativeMonths { get; } - - public bool MsgParamShouldShareStreak { get; } - - public string MsgParamStreakMonths { get; } - - /// Property representing the raw IRC message (for debugging/customized parsing) - public string RawIrc { get; } - - /// Property representing system message. - public string ResubMessage { get; } - - /// Property representing the room id. - public string RoomId { get; } - - /// Property representing the plan a user is on. - public SubscriptionPlan SubscriptionPlan { get; } = SubscriptionPlan.NotSet; - - /// Property representing the subscription plan name. - public string SubscriptionPlanName { get; } - - /// Property representing internval system message value. - public string SystemMessage { get; } - - /// Property representing internal system message value, parsed. - public string SystemMessageParsed { get; } - - /// Property representing the tmi-sent-ts value. - public string TmiSentTs { get; } - - /// Property representing the user's id. - public string UserId { get; } - - /// Property representing the user type of the resubscriber. - public UserType UserType { get; } - - public string Channel { get; } - - // @badges=subscriber/1,turbo/1;color=#2B119C;display-name=JustFunkIt;emotes=;id=9dasn-asdibas-asdba-as8as;login=justfunkit;mod=0;msg-id=resub;msg-param-months=2;room-id=44338537;subscriber=1;system-msg=JustFunkIt\ssubscribed\sfor\s2\smonths\sin\sa\srow!;turbo=1;user-id=26526370;user-type= :tmi.twitch.tv USERNOTICE #burkeblack :AVAST YEE SCURVY DOG - - protected readonly int monthsInternal; - - /// Subscriber object constructor. - protected SubscriberBase(IrcMessage ircMessage) - { - RawIrc = ircMessage.ToString(); - ResubMessage = ircMessage.Message; - - foreach (var tag in ircMessage.Tags.Keys) - { - var tagValue = ircMessage.Tags[tag]; - switch (tag) - { - case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); - // iterate through badges for special circumstances - foreach (var badge in Badges) - { - if (badge.Key == "partner") - IsPartner = true; - } - break; - case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); - break; - case Tags.Color: - ColorHex = tagValue; - if (!string.IsNullOrEmpty(ColorHex)) - Color = ColorTranslator.FromHtml(ColorHex); - break; - case Tags.DisplayName: - DisplayName = tagValue; - break; - case Tags.Emotes: - EmoteSet = tagValue; - break; - case Tags.Id: - Id = tagValue; - break; - case Tags.Login: - Login = tagValue; - break; - case Tags.Mod: - IsModerator = ConvertToBool(tagValue); - break; - case Tags.MsgId: - MsgId = tagValue; - break; - case Tags.MsgParamCumulativeMonths: - MsgParamCumulativeMonths = tagValue; - break; - case Tags.MsgParamStreakMonths: - MsgParamStreakMonths = tagValue; - break; - case Tags.MsgParamShouldShareStreak: - MsgParamShouldShareStreak = Common.Helpers.ConvertToBool(tagValue); - break; - case Tags.MsgParamSubPlan: - switch (tagValue.ToLower()) - { - case "prime": - SubscriptionPlan = SubscriptionPlan.Prime; - break; - case "1000": - SubscriptionPlan = SubscriptionPlan.Tier1; - break; - case "2000": - SubscriptionPlan = SubscriptionPlan.Tier2; - break; - case "3000": - SubscriptionPlan = SubscriptionPlan.Tier3; - break; - default: - throw new ArgumentOutOfRangeException(nameof(tagValue.ToLower)); - } - break; - case Tags.MsgParamSubPlanName: - SubscriptionPlanName = tagValue.Replace("\\s", " "); - break; - case Tags.RoomId: - RoomId = tagValue; - break; - case Tags.Subscriber: - IsSubscriber = ConvertToBool(tagValue); - break; - case Tags.SystemMsg: - SystemMessage = tagValue; - SystemMessageParsed = tagValue.Replace("\\s", " "); - break; - case Tags.TmiSentTs: - TmiSentTs = tagValue; - break; - case Tags.Turbo: - IsTurbo = ConvertToBool(tagValue); - break; - case Tags.UserId: - UserId = tagValue; - break; - case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } - break; - } - } - } - - internal SubscriberBase( - List> badges, - List> badgeInfo, - string colorHex, - Color color, - string displayName, - string emoteSet, - string id, - string login, - string systemMessage, - string msgId, - string msgParamCumulativeMonths, - string msgParamStreakMonths, - bool msgParamShouldShareStreak, - string systemMessageParsed, - string resubMessage, - SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, - string roomId, - string userId, - bool isModerator, - bool isTurbo, - bool isSubscriber, - bool isPartner, - string tmiSentTs, - UserType userType, - string rawIrc, - string channel, - int months) - { - Badges = badges; - BadgeInfo = badgeInfo; - ColorHex = colorHex; - Color = color; - DisplayName = displayName; - EmoteSet = emoteSet; - Id = id; - Login = login; - MsgId = msgId; - MsgParamCumulativeMonths = msgParamCumulativeMonths; - MsgParamStreakMonths = msgParamStreakMonths; - MsgParamShouldShareStreak = msgParamShouldShareStreak; - SystemMessage = systemMessage; - SystemMessageParsed = systemMessageParsed; - ResubMessage = resubMessage; - SubscriptionPlan = subscriptionPlan; - SubscriptionPlanName = subscriptionPlanName; - RoomId = roomId; - UserId = UserId; - IsModerator = isModerator; - IsTurbo = isTurbo; - IsSubscriber = isSubscriber; - IsPartner = isPartner; - TmiSentTs = tmiSentTs; - UserType = userType; - RawIrc = rawIrc; - monthsInternal = months; - UserId = userId; - Channel = channel; - } - - private static bool ConvertToBool(string data) - { - return data == "1"; - } - - /// Overriden ToString method, prints out all properties related to resub. - public override string ToString() - { - return $"Badges: {Badges.Count}, color hex: {ColorHex}, display name: {DisplayName}, emote set: {EmoteSet}, login: {Login}, system message: {SystemMessage}, msgId: {MsgId}, msgParamCumulativeMonths: {MsgParamCumulativeMonths}" + - $"msgParamStreakMonths: {MsgParamStreakMonths}, msgParamShouldShareStreak: {MsgParamShouldShareStreak}, resub message: {ResubMessage}, months: {monthsInternal}, room id: {RoomId}, user id: {UserId}, mod: {IsModerator}, turbo: {IsTurbo}, sub: {IsSubscriber}, user type: {UserType}, raw irc: {RawIrc}"; - } - } -} diff --git a/TwitchLib.Client.Models/TwitchLib.Client.Models.csproj b/TwitchLib.Client.Models/TwitchLib.Client.Models.csproj index 36936e20..ea2f7b87 100644 --- a/TwitchLib.Client.Models/TwitchLib.Client.Models.csproj +++ b/TwitchLib.Client.Models/TwitchLib.Client.Models.csproj @@ -1,30 +1,26 @@  - netstandard2.0 - TwitchLib.Client.Models - 3.4.0 - $(VersionSuffix) + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 Project contains all of the models used in TwitchLib.Client. - true - swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin - https://cdn.syzuna-programs.de/images/twitchlib.png - https://github.com/TwitchLib/TwitchLib.Client - https://opensource.org/licenses/MIT - Copyright 2023 - Incremental changes. See commit history. - https://github.com/TwitchLib/TwitchLib.Client - Git - twitch library irc chat c# csharp api events pubsub net standard 2.0 - en-US - 3.4.0 - 3.4.0 - True + + + + + + <_Parameter1>TwitchLib.Client + + + <_Parameter1>TwitchLib.Client.Test + + + - + + diff --git a/TwitchLib.Client.Models/TwitchLibMessage.cs b/TwitchLib.Client.Models/TwitchLibMessage.cs index e8ac392a..40ab5437 100644 --- a/TwitchLib.Client.Models/TwitchLibMessage.cs +++ b/TwitchLib.Client.Models/TwitchLibMessage.cs @@ -1,44 +1,43 @@ -using System.Collections.Generic; -using System.Drawing; - -using TwitchLib.Client.Enums; +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Interfaces; namespace TwitchLib.Client.Models { /// Class represents Message. - public abstract class TwitchLibMessage + public abstract class TwitchLibMessage : IHexColorProperty { /// List of key-value pair badges. - public List> Badges { get; protected set; } + public List> Badges { get; protected set; } = default!; /// Twitch username of the bot that received the message. - public string BotUsername { get; protected set; } + public string BotUsername { get; protected set; } = default!; /// Property representing HEX color as a System.Drawing.Color object. - public Color Color { get; protected set; } - - /// Hex representation of username color in chat (THIS CAN BE NULL IF VIEWER HASN'T SET COLOR). - public string ColorHex { get; protected set; } + public string HexColor { get; protected set; } = default!; /// Case-sensitive username of sender of chat message. - public string DisplayName { get; protected set; } + public string DisplayName { get; protected set; } = default!; /// Emote Ids that exist in message. - public EmoteSet EmoteSet { get; protected set; } - - /// Twitch site-wide turbo status. - public bool IsTurbo { get; protected set; } + public EmoteSet EmoteSet { get; protected set; } = default!; /// Twitch-unique integer assigned on per account basis. - public string UserId { get; protected set; } + public string UserId { get; protected set; } = default!; /// Username of sender of chat message. - public string Username { get; protected set; } + public string Username { get; protected set; } = default!; + + public UserDetail UserDetail { get; protected set; } /// User type can be viewer, moderator, global mod, admin, or staff public UserType UserType { get; protected set; } /// Raw IRC-style text received from Twitch. - public string RawIrcMessage { get; protected set; } + public string RawIrcMessage { get; protected set; } = default!; + + /// + /// Contains undocumented tags. + /// + public Dictionary? UndocumentedTags { get; protected set; } } } diff --git a/TwitchLib.Client.Models/UnraidNotification.cs b/TwitchLib.Client.Models/UnraidNotification.cs new file mode 100644 index 00000000..dc8864b2 --- /dev/null +++ b/TwitchLib.Client.Models/UnraidNotification.cs @@ -0,0 +1,57 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public class UnraidNotification : UserNoticeBase +{ + /// + /// Initializes a new instance of the class. + /// + public UnraidNotification(IrcMessage ircMessage) : base(ircMessage) + { + } + + /// + /// Initializes a new instance of the class. + /// + public UnraidNotification( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags) + : base(badgeInfo, + badges, + hexColor, + displayName, + emotes, + id, + login, + msgId, + roomId, + systemMsg, + tmiSent, + userDetail, + userId, + userType, + undocumentedTags) + { + } + + /// + protected override bool TrySet(KeyValuePair tag) + { + return false; + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Models/UserBan.cs b/TwitchLib.Client.Models/UserBan.cs index 81fc9ba4..1278d32e 100644 --- a/TwitchLib.Client.Models/UserBan.cs +++ b/TwitchLib.Client.Models/UserBan.cs @@ -4,32 +4,26 @@ namespace TwitchLib.Client.Models { public class UserBan { - /// Reason for ban, if it was provided. - public string BanReason; - /// Channel that had ban event. - public string Channel; + public string Channel { get; } /// User that was banned. - public string Username; + public string Username { get; } /// Channel that had ban event. Id. - public string RoomId; + public string RoomId { get; } = default!; /// User that was banned. Id. - public string TargetUserId; + public string TargetUserId { get; } = default!; + /// + /// Initializes a new instance of the class. + /// public UserBan(IrcMessage ircMessage) { Channel = ircMessage.Channel; Username = ircMessage.Message; - var successBanReason = ircMessage.Tags.TryGetValue(Tags.BanReason, out var banReason); - if (successBanReason) - { - BanReason = banReason; - } - if (ircMessage.Tags.TryGetValue(Tags.RoomId, out var roomId)) { RoomId = roomId; @@ -41,16 +35,17 @@ public UserBan(IrcMessage ircMessage) } } + /// + /// Initializes a new instance of the class. + /// public UserBan( string channel, string username, - string banReason, string roomId, string targetUserId) { Channel = channel; Username = username; - BanReason = banReason; RoomId = roomId; TargetUserId = targetUserId; } diff --git a/TwitchLib.Client.Models/UserDetails.cs b/TwitchLib.Client.Models/UserDetails.cs new file mode 100644 index 00000000..ed8dc2dd --- /dev/null +++ b/TwitchLib.Client.Models/UserDetails.cs @@ -0,0 +1,62 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Extractors; + +namespace TwitchLib.Client.Models; + +public readonly struct UserDetail +{ + readonly UserDetails _flags; + + /// + /// Initializes a new instance of the class. + /// + public UserDetail(UserDetails userDetails) + { + _flags = userDetails; + } + + /// + /// Initializes a new instance of the class. + /// + internal UserDetail(UserDetails userDetails, List> badges) + { + var badgesUserDetails = UserDetailsExtractor.Extract(badges); + _flags = userDetails | badgesUserDetails; + } + + /// + /// A Boolean value that determines whether the user is a moderator. + /// + public bool IsModerator => _flags.HasFlag(UserDetails.Moderator); + + /// + /// A Boolean value that determines whether the user is a subscriber. + /// + public bool IsSubscriber => _flags.HasFlag(UserDetails.Subscriber); + + /// + /// A Boolean value that indicates whether the user has site-wide commercial free mode enabled. + /// + public bool HasTurbo => _flags.HasFlag(UserDetails.Turbo); + + /// + /// Message is from channel VIP. + /// + public bool IsVip => _flags.HasFlag(UserDetails.Vip); + + /// + /// Message is from a Twitch Partner. + /// + public bool IsPartner => _flags.HasFlag(UserDetails.Partner); + + /// + /// Message is from a Twitch Staff member. + /// + public bool IsStaff => _flags.HasFlag(UserDetails.Staff); + + /// + public override string ToString() + { + return _flags.ToString(); + } +} diff --git a/TwitchLib.Client.Models/UserNoticeBase.cs b/TwitchLib.Client.Models/UserNoticeBase.cs new file mode 100644 index 00000000..1a5eaf30 --- /dev/null +++ b/TwitchLib.Client.Models/UserNoticeBase.cs @@ -0,0 +1,186 @@ +using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Interfaces; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Models; + +public abstract class UserNoticeBase : IHexColorProperty +{ + internal const string AnonymousGifterUserId = "274598607"; + + /// + /// Contains metadata related to the chat badges in the tag. + /// + public List> BadgeInfo { get; protected set; } = default!; + + /// + /// List of chat badges. + /// + public List> Badges { get; protected set; } = default!; + + /// + public string HexColor { get; protected set; } = default!; + + /// + /// The user’s display name, escaped as described in the IRCv3 spec. + /// + public string DisplayName { get; protected set; } = default!; + + /// + /// List of emotes and their positions in the message. + /// + public string Emotes { get; protected set; } = default!; + + /// + /// An ID that uniquely identifies this message. + /// + public string Id { get; protected set; } = default!; + + /// + /// The login name of the user whose action generated the message. + /// + public string Login { get; protected set; } = default!; + + /// + /// The type of notice (not the ID). + /// + public string MsgId { get; protected set; } = default!; + + /// + /// An ID that identifies the chat room (channel). + /// + public string RoomId { get; protected set; } = default!; + + /// + /// The message Twitch shows in the chat room for this notice. + /// + public string SystemMsg { get; protected set; } = default!; + + /// + /// The time for when the Twitch IRC server received the message. + /// + public DateTimeOffset TmiSent { get; protected set; } + + public UserDetail UserDetail { get; protected set; } + + /// + /// The user’s ID. + /// + public string UserId { get; protected set; } = default!; + + /// + /// he type of user sending the whisper message. + /// + public UserType UserType { get; protected set; } + + public Dictionary? UndocumentedTags { get; protected set; } + + /// + /// Initializes a new instance of the class. + /// + protected UserNoticeBase(IrcMessage ircMessage) + { + var userDetails = UserDetails.None; + foreach (var tag in ircMessage.Tags) + { + switch (tag.Key) + { + case Tags.BadgeInfo: + BadgeInfo = TagHelper.ToBadges(tag.Value); + break; + case Tags.Badges: + Badges = TagHelper.ToBadges(tag.Value); + break; + case Tags.Color: + HexColor = tag.Value; + break; + case Tags.DisplayName: + DisplayName = tag.Value; + break; + case Tags.Emotes: + Emotes = tag.Value; + break; + case Tags.Id: + Id = tag.Value; + break; + case Tags.Login: + Login = tag.Value; + break; + case Tags.Mod: + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Moderator; + break; + case Tags.MsgId: + MsgId = tag.Value; + break; + case Tags.RoomId: + RoomId = tag.Value; + break; + case Tags.Subscriber: + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Subscriber; + break; + case Tags.SystemMsg: + SystemMsg = tag.Value.Replace("\\s", " "); + break; + case Tags.TmiSentTs: + TmiSent = TagHelper.ToDateTimeOffsetFromUnixMs(tag.Value); + break; + case Tags.Turbo: + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Turbo; + break; + case Tags.UserId: + UserId = tag.Value; + break; + case Tags.UserType: + UserType = TagHelper.ToUserType(tag.Value); + break; + default: + if (!TrySet(tag)) + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); + break; + } + } + UserDetail = new UserDetail(userDetails, Badges); + } + + /// + /// Initializes a new instance of the class. + /// + protected UserNoticeBase( + List> badgeInfo, + List> badges, + string hexColor, + string displayName, + string emotes, + string id, + string login, + string msgId, + string roomId, + string systemMsg, + DateTimeOffset tmiSent, + UserDetail userDetail, + string userId, + UserType userType, + Dictionary? undocumentedTags) + { + BadgeInfo = badgeInfo; + Badges = badges; + HexColor = hexColor; + DisplayName = displayName; + Emotes = emotes; + Id = id; + Login = login; + MsgId = msgId; + RoomId = roomId; + SystemMsg = systemMsg; + TmiSent = tmiSent; + UserDetail = userDetail; + UserId = userId; + UserType = userType; + UndocumentedTags = undocumentedTags; + } + + protected abstract bool TrySet(KeyValuePair tag); +} diff --git a/TwitchLib.Client.Models/UserState.cs b/TwitchLib.Client.Models/UserState.cs index 7d404ca7..de3b5bca 100644 --- a/TwitchLib.Client.Models/UserState.cs +++ b/TwitchLib.Client.Models/UserState.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; - using TwitchLib.Client.Enums; +using TwitchLib.Client.Models.Interfaces; using TwitchLib.Client.Models.Internal; namespace TwitchLib.Client.Models { /// Class representing state of a specific user. - public class UserState + public class UserState : IHexColorProperty { /// Properrty representing the chat badges a specific user has. public List> Badges { get; } = new List>(); @@ -19,16 +17,16 @@ public class UserState public string Channel { get; } /// Properrty representing HEX user's name. - public string ColorHex { get; } + public string HexColor { get; } = default!; /// Property representing user's display name. - public string DisplayName { get; } + public string DisplayName { get; } = default!; /// Property representing emote sets available to user. - public string EmoteSet { get; } - + public string EmoteSet { get; } = default!; + /// Property representing the user's Id. - public string Id { get; } + public string Id { get; } = default!; /// Property representing Turbo status. public bool IsModerator { get; } @@ -36,9 +34,19 @@ public class UserState /// Property representing subscriber status. public bool IsSubscriber { get; } + /// + /// A Boolean value that indicates whether the user has site-wide commercial free mode enabled. + /// + public bool Turbo { get; } + /// Property representing returned user type of user. public UserType UserType { get; } + /// + /// Contains undocumented tags. + /// + public Dictionary? UndocumentedTags { get; } + /// /// Constructor for UserState. /// @@ -47,19 +55,19 @@ public UserState(IrcMessage ircMessage) { Channel = ircMessage.Channel; - foreach (var tag in ircMessage.Tags.Keys) + foreach (var tag in ircMessage.Tags) { - var tagValue = ircMessage.Tags[tag]; - switch (tag) + var tagValue = tag.Value; + switch (tag.Key) { case Tags.Badges: - Badges = Common.Helpers.ParseBadges(tagValue); + Badges = TagHelper.ToBadges(tagValue); break; case Tags.BadgeInfo: - BadgeInfo = Common.Helpers.ParseBadges(tagValue); + BadgeInfo = TagHelper.ToBadges(tagValue); break; case Tags.Color: - ColorHex = tagValue; + HexColor = tagValue; break; case Tags.DisplayName: DisplayName = tagValue; @@ -71,34 +79,19 @@ public UserState(IrcMessage ircMessage) Id = tagValue; break; case Tags.Mod: - IsModerator = Common.Helpers.ConvertToBool(tagValue); + IsModerator = TagHelper.ToBool(tagValue); break; case Tags.Subscriber: - IsSubscriber = Common.Helpers.ConvertToBool(tagValue); + IsSubscriber = TagHelper.ToBool(tagValue); + break; + case Tags.Turbo: + Turbo = TagHelper.ToBool(tagValue); break; case Tags.UserType: - switch (tagValue) - { - case "mod": - UserType = UserType.Moderator; - break; - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } + UserType = TagHelper.ToUserType(tag.Value); break; default: - // This should never happen, unless Twitch changes their shit - Console.WriteLine($"Unaccounted for [UserState]: {tag}"); + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); break; } } @@ -107,10 +100,13 @@ public UserState(IrcMessage ircMessage) UserType = UserType.Broadcaster; } + /// + /// Initializes a new instance of the class. + /// public UserState( List> badges, List> badgeInfo, - string colorHex, + string hexColor, string displayName, string emoteSet, string channel, @@ -121,7 +117,7 @@ public UserState( { Badges = badges; BadgeInfo = badgeInfo; - ColorHex = colorHex; + HexColor = hexColor; DisplayName = displayName; EmoteSet = emoteSet; Channel = channel; diff --git a/TwitchLib.Client.Models/UserTimeout.cs b/TwitchLib.Client.Models/UserTimeout.cs index f58de55c..1ceded1f 100644 --- a/TwitchLib.Client.Models/UserTimeout.cs +++ b/TwitchLib.Client.Models/UserTimeout.cs @@ -5,56 +5,62 @@ namespace TwitchLib.Client.Models public class UserTimeout { /// Channel that had timeout event. - public string Channel; + public string Channel { get; } - /// Duration of timeout IN SECONDS. - public int TimeoutDuration; - - /// Reason for timeout, if it was provided. - public string TimeoutReason; + /// Duration of timeout + public TimeSpan TimeoutDuration { get; } /// Viewer that was timed out. - public string Username; + public string Username { get; } /// Id of Viewer that was timed out. - public string TargetUserId; + public string TargetUserId { get; } = default!; + + /// + /// Contains undocumented tags. + /// + public Dictionary? UndocumentedTags { get; } + /// + /// Initializes a new instance of the class. + /// public UserTimeout(IrcMessage ircMessage) { Channel = ircMessage.Channel; Username = ircMessage.Message; - foreach (var tag in ircMessage.Tags.Keys) + foreach (var tag in ircMessage.Tags) { - var tagValue = ircMessage.Tags[tag]; + var tagValue = tag.Value; - switch (tag) + switch (tag.Key) { case Tags.BanDuration: - TimeoutDuration = int.Parse(tagValue); - break; - case Tags.BanReason: - TimeoutReason = tagValue; + TimeoutDuration = TimeSpan.FromSeconds(int.Parse(tagValue)); break; case Tags.TargetUserId: TargetUserId = tagValue; break; + default: + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); + break; } } } + /// + /// Initializes a new instance of the class. + /// public UserTimeout( string channel, string username, - string targetuserId, - int timeoutDuration, - string timeoutReason) + string targetUserId, + TimeSpan timeoutDuration) { Channel = channel; Username = username; - TargetUserId = targetuserId; + TargetUserId = targetUserId; TimeoutDuration = timeoutDuration; - TimeoutReason = timeoutReason; } } } diff --git a/TwitchLib.Client.Models/WhisperCommand.cs b/TwitchLib.Client.Models/WhisperCommand.cs deleted file mode 100644 index be5e434d..00000000 --- a/TwitchLib.Client.Models/WhisperCommand.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace TwitchLib.Client.Models -{ - /// Object representing a command received via Twitch chat. - public class WhisperCommand - { - /// Property representing all arguments received in a List form. - public List ArgumentsAsList { get; } - - /// Property representing all arguments received in a string form. - public string ArgumentsAsString { get; } - - /// Property representing the command identifier (ie command prefix). - public char CommandIdentifier { get; } - - /// Property representing the actual command (without the command prefix). - public string CommandText { get; } - - /// Property representing the chat message that the command came in. - public WhisperMessage WhisperMessage { get; } - - /// ChatCommand constructor. - /// - public WhisperCommand(WhisperMessage whisperMessage) - { - WhisperMessage = whisperMessage; - CommandText = whisperMessage.Message.Split(' ')?[0].Substring(1, whisperMessage.Message.Split(' ')[0].Length - 1) ?? whisperMessage.Message.Substring(1, whisperMessage.Message.Length - 1); - ArgumentsAsString = whisperMessage.Message.Replace(whisperMessage.Message.Split(' ')?[0] + " ", ""); - ArgumentsAsList = whisperMessage.Message.Split(' ')?.Where(arg => arg != whisperMessage.Message[0] + CommandText).ToList() ?? new List(); - CommandIdentifier = whisperMessage.Message[0]; - } - - public WhisperCommand( - WhisperMessage whisperMessage, - string commandText, - string argumentsAsString, - List argumentsAsList, - char commandIdentifier) - { - WhisperMessage = whisperMessage; - CommandText = commandText; - ArgumentsAsString = argumentsAsString; - ArgumentsAsList = argumentsAsList; - CommandIdentifier = commandIdentifier; - } - } -} diff --git a/TwitchLib.Client.Models/WhisperMessage.cs b/TwitchLib.Client.Models/WhisperMessage.cs index 9c56963a..59b222ea 100644 --- a/TwitchLib.Client.Models/WhisperMessage.cs +++ b/TwitchLib.Client.Models/WhisperMessage.cs @@ -1,9 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; - using TwitchLib.Client.Enums; -using TwitchLib.Client.Models.Extensions.NetCore; using TwitchLib.Client.Models.Internal; namespace TwitchLib.Client.Models @@ -12,39 +7,40 @@ namespace TwitchLib.Client.Models public class WhisperMessage : TwitchLibMessage { /// Property representing message identifier. - public string MessageId { get; } + public string MessageId { get; } = default!; /// Property representing identifier of the message thread. - public string ThreadId { get; } + public string ThreadId { get; } = default!; /// Property representing identifier of the message thread. public string Message { get; } + /// + /// Initializes a new instance of the class. + /// public WhisperMessage( List> badges, - string colorHex, - Color color, + string hexColor, string username, string displayName, EmoteSet emoteSet, string threadId, string messageId, string userId, - bool isTurbo, string botUsername, string message, + UserDetail userDetail, UserType userType) { Badges = badges; - ColorHex = colorHex; - Color = color; + HexColor = hexColor; Username = username; DisplayName = displayName; EmoteSet = emoteSet; ThreadId = threadId; MessageId = messageId; UserId = userId; - IsTurbo = isTurbo; + UserDetail = userDetail; BotUsername = botUsername; Message = message; UserType = userType; @@ -62,26 +58,17 @@ public WhisperMessage(IrcMessage ircMessage, string botUsername) RawIrcMessage = ircMessage.ToString(); Message = ircMessage.Message; - foreach (var tag in ircMessage.Tags.Keys) + var userDetails = UserDetails.None; + foreach (var tag in ircMessage.Tags) { - var tagValue = ircMessage.Tags[tag]; - switch (tag) + var tagValue = tag.Value; + switch (tag.Key) { case Tags.Badges: - Badges = new List>(); - if (tagValue.Contains('/')) - { - if (!tagValue.Contains(",")) - Badges.Add(new KeyValuePair(tagValue.Split('/')[0], tagValue.Split('/')[1])); - else - foreach (var badge in tagValue.Split(',')) - Badges.Add(new KeyValuePair(badge.Split('/')[0], badge.Split('/')[1])); - } + Badges = TagHelper.ToBadges(tagValue); break; case Tags.Color: - ColorHex = tagValue; - if (!string.IsNullOrEmpty(ColorHex)) - Color = ColorTranslator.FromHtml(ColorHex); + HexColor = tagValue; break; case Tags.DisplayName: DisplayName = tagValue; @@ -96,33 +83,23 @@ public WhisperMessage(IrcMessage ircMessage, string botUsername) ThreadId = tagValue; break; case Tags.Turbo: - IsTurbo = Common.Helpers.ConvertToBool(tagValue); + if (TagHelper.ToBool(tag.Value)) + userDetails |= UserDetails.Turbo; break; case Tags.UserId: UserId = tagValue; break; case Tags.UserType: - switch (tagValue) - { - case "global_mod": - UserType = UserType.GlobalModerator; - break; - case "admin": - UserType = UserType.Admin; - break; - case "staff": - UserType = UserType.Staff; - break; - default: - UserType = UserType.Viewer; - break; - } + UserType = TagHelper.ToUserType(tag.Value); + break; + default: + (UndocumentedTags ??= new()).Add(tag.Key, tag.Value); break; } } + UserDetail = new(userDetails, Badges); - if (EmoteSet == null) - EmoteSet = new EmoteSet(default(string), Message); + EmoteSet ??= new EmoteSet(default(string), Message); } } } diff --git a/TwitchLib.Client.Test/CommandInfoTest.cs b/TwitchLib.Client.Test/CommandInfoTest.cs new file mode 100644 index 00000000..d7571f62 --- /dev/null +++ b/TwitchLib.Client.Test/CommandInfoTest.cs @@ -0,0 +1,35 @@ +using TwitchLib.Client.Models; +using Xunit; + +namespace TwitchLib.Client.Test; + +public class CommandInfoTest +{ + [Theory] + [InlineData("", "")] + [InlineData("!", "!")] + [InlineData("!", "! command")] + [InlineData("?", "!command")] + public void ParsingFailAndReturnNull(string commandIdentifier, string message) + { + 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.Test/MockIClient.cs b/TwitchLib.Client.Test/MockIClient.cs index 1af8efd5..79338ce8 100644 --- a/TwitchLib.Client.Test/MockIClient.cs +++ b/TwitchLib.Client.Test/MockIClient.cs @@ -1,65 +1,54 @@ using System; +using System.Threading.Tasks; using TwitchLib.Communication.Events; using TwitchLib.Communication.Interfaces; +using TwitchLib.Communication.Models; namespace TwitchLib.Client.Test { public class MockIClient : IClient { - public void WhisperThrottled(OnWhisperThrottledEventArgs eventArgs) - { - throw new NotImplementedException(); - } - public TimeSpan DefaultKeepAliveInterval { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public int SendQueueLength => throw new NotImplementedException(); public bool IsConnected { get; private set; } - public IClientOptions Options { get; set; } + public IClientOptions Options { get; set; } = new ClientOptions(); public int WhisperQueueLength => throw new NotImplementedException(); - public event EventHandler OnConnected; - public event EventHandler OnData; - public event EventHandler OnDisconnected; - public event EventHandler OnError; - public event EventHandler OnFatality; - public event EventHandler OnMessage; - public event EventHandler OnSendFailed; - public event EventHandler OnStateChanged; - public event EventHandler OnReconnected; - public event EventHandler OnMessageThrottled; - public event EventHandler OnWhisperThrottled; + public event AsyncEventHandler? OnConnected; + public event AsyncEventHandler? OnDisconnected; + public event AsyncEventHandler? OnError; + public event AsyncEventHandler? OnFatality; + public event AsyncEventHandler? OnMessage; + public event AsyncEventHandler? OnSendFailed; + public event AsyncEventHandler? OnReconnected; - public void Close(bool callDisconnect = true) + public Task CloseAsync() { IsConnected = false; OnDisconnected?.Invoke(this, new OnDisconnectedEventArgs()); + return Task.CompletedTask; } public void Dispose() { } - public void Dispose(bool waitForSendsToComplete) - { } - - public bool Open() + public async Task OpenAsync() { IsConnected = true; - OnConnected?.Invoke(this, new OnConnectedEventArgs()); + if (OnConnected is not null) + await OnConnected.Invoke(this, new OnConnectedEventArgs()); return true; } - public void Reconnect() + public async Task ReconnectAsync() { IsConnected = true; - OnReconnected?.Invoke(this, new OnReconnectedEventArgs()); - } - - public void MessageThrottled(OnMessageThrottledEventArgs eventArgs) - { - throw new NotImplementedException(); + if (OnReconnected is not null) + await OnReconnected.Invoke(this, new OnConnectedEventArgs()); + return true; } public void SendFailed(OnSendFailedEventArgs eventArgs) @@ -72,14 +61,15 @@ public void Error(OnErrorEventArgs eventArgs) throw new NotImplementedException(); } - public bool Send(string data) + public Task SendAsync(string data) { - return true; + return Task.FromResult(true); } - public void ReceiveMessage(string message) + public async Task ReceiveMessage(string message) { - OnMessage?.Invoke(this, new OnMessageEventArgs { Message = message }); + if (OnMessage is not null) + await OnMessage.Invoke(this, new OnMessageEventArgs(message)); } public bool SendWhisper(string data) diff --git a/TwitchLib.Client.Test/MyAssert.cs b/TwitchLib.Client.Test/MyAssert.cs new file mode 100644 index 00000000..2c48b6b9 --- /dev/null +++ b/TwitchLib.Client.Test/MyAssert.cs @@ -0,0 +1,99 @@ +using System; +using Xunit; +using TwitchLib.Communication.Events; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace TwitchLib.Client.Test; + +//TL;DR: XUNIT with modification to accept async event Handler +public partial class MyAssert +{ + /// + /// Verifies that a event with the exact event args (and not a derived type) is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw RaisesException.ForNoEvent(typeof(T)); + + if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) + throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); + + return raisedEvent; + } + + /// + /// Verifies that an event with the exact or a derived event args is raised. + /// + /// The type of the event arguments to expect + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync(Action> attach, Action> detach, Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw RaisesException.ForNoEvent(typeof(T)); + + return raisedEvent; + } + + static async Task?> RaisesAsyncInternal(Action> attach, Action> detach, Func testCode) + { + Assert.NotNull(attach); + Assert.NotNull(detach); + Assert.NotNull(testCode); + + RaisedEvent? raisedEvent = null; + AsyncEventHandler value = (object? s, T args) => + { + raisedEvent = new RaisedEvent(s, args); + return Task.CompletedTask; + }; + AsyncEventHandler handler = value; + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + + /// + /// Represents a raised event after the fact. + /// + /// The type of the event arguments. + public class RaisedEvent + { + /// + /// The sender of the event. + /// + public object? Sender { get; } + + /// + /// The event arguments. + /// + public T Arguments { get; } + + /// + /// Creates a new instance of the class. + /// + /// The sender of the event. + /// The event arguments + public RaisedEvent(object? sender, T args) + { + Sender = sender; + Arguments = args; + } + } +} diff --git a/TwitchLib.Client.Test/Parsing.cs b/TwitchLib.Client.Test/Parsing.cs new file mode 100644 index 00000000..e3153224 --- /dev/null +++ b/TwitchLib.Client.Test/Parsing.cs @@ -0,0 +1,54 @@ +using TwitchLib.Client.Models.Extractors; +using TwitchLib.Client.Models.Internal; +using Xunit; + +namespace TwitchLib.Client.Test; + +public class Parsing +{ + TwitchClient _twitchClient = new(); + + public Parsing() + { + _twitchClient.Initialize(new("username", "password"), "channel"); + } + + [Fact] + public void Badges() + { + var badges = "broadcaster/1;moderator/1;premium/1;vip/1;no_audio/1;no_video/1;turbo/1;partner/1;" + + "bits/1000;hype-train/2;moments/5;predictions/blue-1;predictions/pink-2;subscriber/18;sub-gifter/150;" + + "twitchconEU2023/1;bits-charity/1;founder/0;glhf-pledge/1;superultracombo-2023/1"; + TagHelper.ToBadges(badges); + } + + [Fact] + public void EmoteExtractorExtract() + { + var message = "One 😂 Two Kappa Three"; + var emotes = EmoteExtractor.Extract("25:10-14", message); + Assert.True(emotes.Count == 1); + Assert.True(emotes[0].Name == "Kappa"); + Assert.True(emotes[0].StartIndex == message.IndexOf("Kappa")); + } + + + [Theory] + [InlineData("@ban-duration=350;room-id=12345678;target-user-id=87654321;tmi-sent-ts=1642719320727 :tmi.twitch.tv CLEARCHAT #dallas :ronni")] + [InlineData("@room-id=12345678;tmi-sent-ts=1642715695392 :tmi.twitch.tv CLEARCHAT #dallas")] + [InlineData("@room-id=12345678;target-user-id=87654321;tmi-sent-ts=1642715756806 :tmi.twitch.tv CLEARCHAT #dallas :ronni")] + [InlineData("@login=ronni;room-id=;target-msg-id=abc-123-def;tmi-sent-ts=1642720582342 :tmi.twitch.tv CLEARMSG #dallas :HeyGuys")] + [InlineData("@badge-info=subscriber/8;badges=subscriber/6;color=#0D4200;display-name=dallas;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;turbo=0;user-id=12345678;user-type=admin :tmi.twitch.tv GLOBALUSERSTATE")] + [InlineData("@badge-info=;badges=turbo/1;color=#0D4200;display-name=ronni;emotes=25:0-4,12-16/1902:6-10;id=b34ccfc7-4977-403a-8a94-33c6bac34fb8;mod=0;room-id=1337;subscriber=0;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=global_mod :ronni!ronni@ronni.tmi.twitch.tv PRIVMSG #ronni :Kappa Keepo Kappa")] + [InlineData("@badge-info=;badges=staff/1,bits/1000;bits=100;color=;display-name=ronni;emotes=;id=b34ccfc7-4977-403a-8a94-33c6bac34fb8;mod=0;room-id=12345678;subscriber=0;tmi-sent-ts=1507246572675;turbo=1;user-id=12345678;user-type=staff :ronni!ronni@ronni.tmi.twitch.tv PRIVMSG #ronni :cheer100")] + [InlineData("@badge-info=;badges=staff/1;color=#0D4200;display-name=ronni;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;mod=1;subscriber=1;turbo=1;user-type=staff :tmi.twitch.tv USERSTATE #dallas")] + [InlineData("@badges=staff/1,bits-charity/1;color=#8A2BE2;display-name=PetsgomOO;emotes=;message-id=306;thread-id=12345678_87654321;turbo=0;user-id=87654321;user-type=staff :petsgomoo!petsgomoo@petsgomoo.tmi.twitch.tv WHISPER foo :hello")] + [InlineData("@badge-info=;badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-cumulative-months=6;msg-param-streak-months=2;msg-param-should-share-streak=1;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=12345678;subscriber=1;system-msg=ronni\\shas\\ssubscribed\\sfor\\s6\\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=87654321;user-type=staff :tmi.twitch.tv USERNOTICE #dallas :Great stream -- keep it up!")] + [InlineData("@badge-info=;badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=55554444;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\\sof\\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=87654321;user-type=staff :tmi.twitch.tv USERNOTICE #forstycup")] + [InlineData("@badge-info=;badges=turbo/1;color=#9ACD32;display-name=TestChannel;emotes=;id=3d830f12-795c-447d-af3c-ea05e40fbddb;login=testchannel;mod=0;msg-id=raid;msg-param-displayName=TestChannel;msg-param-login=testchannel;msg-param-viewerCount=15;room-id=33332222;subscriber=0;system-msg=15\\sraiders\\sfrom\\sTestChannel\\shave\\sjoined\\n!;tmi-sent-ts=1507246572675;turbo=1;user-id=123456;user-type= :tmi.twitch.tv USERNOTICE #othertestchannel")] + [InlineData("@badge-info=;badges=;color=;display-name=SevenTest1;emotes=30259:0-6;id=37feed0f-b9c7-4c3a-b475-21c6c6d21c3d;login=seventest1;mod=0;msg-id=ritual;msg-param-ritual-name=new_chatter;room-id=87654321;subscriber=0;system-msg=Seventoes\\sis\\snew\\shere!;tmi-sent-ts=1508363903826;turbo=0;user-id=77776666;user-type= :tmi.twitch.tv USERNOTICE #seventoes :HeyGuys")] + public void Events(string raw) + { + _twitchClient.OnReadLineTestAsync(raw).Wait(); + } +} diff --git a/TwitchLib.Client.Test/ThrottlingServiceTests.cs b/TwitchLib.Client.Test/ThrottlingServiceTests.cs new file mode 100644 index 00000000..0ee33327 --- /dev/null +++ b/TwitchLib.Client.Test/ThrottlingServiceTests.cs @@ -0,0 +1,37 @@ +using Moq; +using TwitchLib.Client.Interfaces; +using TwitchLib.Client.Models; +using TwitchLib.Client.Throttling; +using TwitchLib.Communication.Interfaces; +using Xunit; + +namespace TwitchLib.Client.Test +{ + public class ThrottlingServiceTests + { + [Theory] + [InlineData(false, 1, true, false)] + [InlineData(true, 0, true, false)] + [InlineData(true, 0, false, false)] + [InlineData(true, 1, true, true)] + [InlineData(true, 1, false, false)] + public void EnqueueMessageTests(bool isConnected, uint queueCapacity, bool withMessage, bool expected) + { + var sendOptions = new SendOptions(20, queueCapacity); + var clientMock = new Mock(); + clientMock.Setup(c => c.IsConnected) + .Returns(isConnected); + var client = clientMock.Object; + var throttlerService = new ThrottlingService(client, sendOptions); + OutboundChatMessage? message = null; + + if (withMessage) + { + message = new OutboundChatMessage("channel", "message"); + } + + var enqueued = throttlerService.Enqueue(message); + Assert.Equal(expected, enqueued); + } + } +} \ No newline at end of file diff --git a/TwitchLib.Client.Test/TwitchClientEventTests.cs b/TwitchLib.Client.Test/TwitchClientEventTests.cs index a4e5abaf..25714d29 100644 --- a/TwitchLib.Client.Test/TwitchClientEventTests.cs +++ b/TwitchLib.Client.Test/TwitchClientEventTests.cs @@ -1,9 +1,9 @@ using System; -using Xunit; -using TwitchLib.Client.Events; -using TwitchLib.Communication.Events; using System.Threading; using System.Threading.Tasks; +using TwitchLib.Client.Events; +using TwitchLib.Communication.Events; +using Xunit; namespace TwitchLib.Client.Test { @@ -19,166 +19,166 @@ public TwitchClientEventTests() } [Fact] - public void ClientCanReceiveData() + public async Task ClientCanReceiveData() { var client = new TwitchClient(_mockClient); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnSendReceiveData += h, h => client.OnSendReceiveData -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - _mockClient.ReceiveMessage($":tmi.twitch.tv 001 {TWITCH_BOT_USERNAME} :Welcome, GLHF!"); + await client.ConnectAsync(); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 001 {TWITCH_BOT_USERNAME} :Welcome, GLHF!"); }); } [Fact] - public void ClientCanJoinChannels() + public async Task ClientCanJoinChannels() { - var client = new TwitchClient( _mockClient); - client.OnConnected += (sender, e) => + var client = new TwitchClient(_mockClient); + client.OnConnected += async (sender, e) => { - client.JoinChannel(TWITCH_CHANNEL); - ReceivedRoomState(); + await client.JoinChannelAsync(TWITCH_CHANNEL); + await ReceivedJoin(TWITCH_CHANNEL); }; - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnJoinedChannel += h, h => client.OnJoinedChannel -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); }); } [Fact] - public void MessageEmoteCollectionFilled() + public async Task MessageEmoteCollectionFilled() { var finish = DateTime.Now.AddSeconds(10); - var client = new TwitchClient( _mockClient); + var client = new TwitchClient(_mockClient); var emoteCount = 0; client.OnConnected += (sender, e) => ReceivedTestMessage(); - client.OnMessageReceived += (sender, e) => emoteCount = e.ChatMessage.EmoteSet.Emotes.Count; + client.OnMessageReceived += async (sender, e) => emoteCount = e.ChatMessage.EmoteSet.Emotes.Count; client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); while (emoteCount == 0 && DateTime.Now < finish) { } - Assert.NotEqual(0, emoteCount); + Assert.NotEqual((double)0, emoteCount, 0); } [Fact] - public void ClientRaisesOnConnected() + public async void ClientRaisesOnConnected() { - var client = new TwitchClient( _mockClient); + var client = new TwitchClient(_mockClient); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnConnected += h, h => client.OnConnected -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); }); } [Fact] - public void ClientRaisesOnMessageReceived() + public async Task ClientRaisesOnMessageReceived() { - var client = new TwitchClient( _mockClient); + var client = new TwitchClient(_mockClient); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnMessageReceived += h, h => client.OnMessageReceived -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTestMessage(); + await client.ConnectAsync(); + await ReceivedTestMessage(); }); } [Fact] - public void ClientRaisesOnJoinedChannel() + public async Task ClientRaisesOnJoinedChannel() { - var client = new TwitchClient( _mockClient); + var client = new TwitchClient(_mockClient); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnJoinedChannel += h, h => client.OnJoinedChannel -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); - client.JoinChannel(TWITCH_CHANNEL); - ReceivedRoomState(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); + await client.JoinChannelAsync(TWITCH_CHANNEL); + await ReceivedJoin(TWITCH_CHANNEL); }); } [Fact] - public void ClientChannelAddedToJoinedChannels() + public async Task ClientChannelAddedToJoinedChannels() { - var client = new TwitchClient( _mockClient); + var client = new TwitchClient(_mockClient); client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); - client.JoinChannel(TWITCH_CHANNEL); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); + await client.JoinChannelAsync(TWITCH_CHANNEL); - Assert.Equal(1, client.JoinedChannels.Count); + Assert.Equal((double)1, client.JoinedChannels.Count,0); } [Fact] - public void ClientRaisesOnDisconnected() + public async Task ClientRaisesOnDisconnected() { var client = new TwitchClient(_mockClient); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnDisconnected += h, h => client.OnDisconnected -= h, - () => + async () => { client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); - client.JoinChannel(TWITCH_CHANNEL); - ReceivedRoomState(); - client.Disconnect(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); + await client.JoinChannelAsync(TWITCH_CHANNEL); + await ReceivedRoomState(); + await client.DisconnectAsync(); }); } [Fact] - public void ClientReconnectsOk() + public async Task ClientReconnectsOk() { var client = new TwitchClient(_mockClient); var pauseConnected = new ManualResetEvent(false); var pauseReconnected = new ManualResetEvent(false); - Assert.Raises( + await MyAssert.RaisesAsync( h => client.OnReconnected += h, h => client.OnReconnected -= h, - () => + async () => { - client.OnConnected += (s, e) => + client.OnConnected += async (s, e) => { pauseConnected.Set(); - client.Disconnect(); + await client.DisconnectAsync(); }; - client.OnDisconnected += (s, e) => { client.Reconnect(); }; - client.OnReconnected += (s, e) => { pauseReconnected.Set(); }; - + client.OnDisconnected += async (s, e) => { await client.ReconnectAsync(); }; + client.OnReconnected += async (s, e) => { pauseReconnected.Set(); }; + client.Initialize(new Models.ConnectionCredentials(TWITCH_BOT_USERNAME, "OAuth")); - client.Connect(); - ReceivedTwitchConnected(); + await client.ConnectAsync(); + await ReceivedTwitchConnected(); Assert.True(pauseConnected.WaitOne(5000)); Assert.True(pauseReconnected.WaitOne(60000)); @@ -186,36 +186,40 @@ public void ClientReconnectsOk() } #region Messages for Tests - - private void ReceivedUserNoticeMessage() + private async Task ReceivedUserNoticeMessage() { - _mockClient.ReceiveMessage("@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 #testchannel kittyjinxu > #testchannel: HeyGuys"); + await _mockClient.ReceiveMessage("@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 #testchannel kittyjinxu > #testchannel: HeyGuys"); } - private void ReceivedTestMessage() + private async Task ReceivedTestMessage() { - _mockClient.ReceiveMessage($"@badges=subscriber/0,premium/1;color=#005C0B;display-name=KIJUI;emotes=30259:0-6;id=fefffeeb-1e87-4adf-9912-ca371a18cbfd;mod=0;room-id=22510310;subscriber=1;tmi-sent-ts=1530128909202;turbo=0;user-id=25517628;user-type= :kijui!kijui@kijui.tmi.twitch.tv PRIVMSG #testchannel :TEST MESSAGE"); + await _mockClient.ReceiveMessage("@badges=subscriber/0,premium/1;color=#005C0B;display-name=KIJUI;emotes=30259:0-6;id=fefffeeb-1e87-4adf-9912-ca371a18cbfd;mod=0;room-id=22510310;subscriber=1;tmi-sent-ts=1530128909202;turbo=0;user-id=25517628;user-type= :kijui!kijui@kijui.tmi.twitch.tv PRIVMSG #testchannel :TEST MESSAGE"); + // await _mockClient.ReceiveMessage(":jtv!jtv@jtv.tmi.twitch.tv PRIVMSG (HOSTED):(HOSTER) is now hosting you for (VIEWERS_TOTAL) viewers."); } - private void ReceivedTwitchConnected() + private async Task ReceivedTwitchConnected() { - _mockClient.ReceiveMessage($":tmi.twitch.tv 001 {TWITCH_BOT_USERNAME} :Welcome, GLHF!"); - _mockClient.ReceiveMessage($":tmi.twitch.tv 002 {TWITCH_BOT_USERNAME} :Your host is tmi.twitch.tv"); - _mockClient.ReceiveMessage($":tmi.twitch.tv 003 {TWITCH_BOT_USERNAME} :This server is rather new"); - _mockClient.ReceiveMessage($":tmi.twitch.tv 004 {TWITCH_BOT_USERNAME} :-"); - _mockClient.ReceiveMessage($":tmi.twitch.tv 375 {TWITCH_BOT_USERNAME} :-"); - _mockClient.ReceiveMessage($":tmi.twitch.tv 372 {TWITCH_BOT_USERNAME} :You are in a maze of twisty passages, all alike."); - _mockClient.ReceiveMessage($":tmi.twitch.tv 376 {TWITCH_BOT_USERNAME} :>"); - _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/membership"); - _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/commands"); - _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/tags"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 001 {TWITCH_BOT_USERNAME} :Welcome, GLHF!"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 002 {TWITCH_BOT_USERNAME} :Your host is tmi.twitch.tv"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 003 {TWITCH_BOT_USERNAME} :This server is rather new"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 004 {TWITCH_BOT_USERNAME} :-"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 375 {TWITCH_BOT_USERNAME} :-"); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 372 {TWITCH_BOT_USERNAME} :You are in a maze of twisty passages, all alike."); + await _mockClient.ReceiveMessage($":tmi.twitch.tv 376 {TWITCH_BOT_USERNAME} :>"); + await _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/membership"); + await _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/commands"); + await _mockClient.ReceiveMessage(":tmi.twitch.tv CAP * ACK :twitch.tv/tags"); } - private void ReceivedRoomState() + private async Task ReceivedRoomState() { - _mockClient.ReceiveMessage($"@broadcaster-lang=;r9k=0;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #{TWITCH_CHANNEL}"); + await _mockClient.ReceiveMessage($"@broadcaster-lang=;r9k=0;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #{TWITCH_CHANNEL}"); } + private async Task ReceivedJoin(string channel) + { + await _mockClient.ReceiveMessage($":{TWITCH_BOT_USERNAME}!{TWITCH_BOT_USERNAME}@{TWITCH_BOT_USERNAME}.tmi.twitch.tv JOIN #{channel}"); + } #endregion - } + } } diff --git a/TwitchLib.Client.Test/TwitchLib.Client.Test.csproj b/TwitchLib.Client.Test/TwitchLib.Client.Test.csproj index 7eb94a39..dc31c566 100644 --- a/TwitchLib.Client.Test/TwitchLib.Client.Test.csproj +++ b/TwitchLib.Client.Test/TwitchLib.Client.Test.csproj @@ -1,17 +1,17 @@  - net5.0 - + net8.0 false - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TwitchLib.Client.sln b/TwitchLib.Client.sln deleted file mode 100644 index 9e02c31b..00000000 --- a/TwitchLib.Client.sln +++ /dev/null @@ -1,53 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Client", "TwitchLib.Client\TwitchLib.Client.csproj", "{44B88EFA-1500-4005-AF27-A9BCB9DD79C1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Client.Models", "TwitchLib.Client.Models\TwitchLib.Client.Models.csproj", "{C77438A0-7745-4F4F-BB8A-CFFA52DC5E18}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Client.Enums", "TwitchLib.Client.Enums\TwitchLib.Client.Enums.csproj", "{BA43ED8E-04B3-4F04-B828-EC5D7684FEED}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitchLib.Client.Test", "TwitchLib.Client.Test\TwitchLib.Client.Test.csproj", "{4E8210DE-33A6-42B0-89F9-ED84A8AC197C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7495B75C-C415-4CED-A4D9-6DE4DE9B0A04}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8C69E628-6CFC-4F25-8CAC-EC63927C0120}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {44B88EFA-1500-4005-AF27-A9BCB9DD79C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44B88EFA-1500-4005-AF27-A9BCB9DD79C1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44B88EFA-1500-4005-AF27-A9BCB9DD79C1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44B88EFA-1500-4005-AF27-A9BCB9DD79C1}.Release|Any CPU.Build.0 = Release|Any CPU - {C77438A0-7745-4F4F-BB8A-CFFA52DC5E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C77438A0-7745-4F4F-BB8A-CFFA52DC5E18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C77438A0-7745-4F4F-BB8A-CFFA52DC5E18}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C77438A0-7745-4F4F-BB8A-CFFA52DC5E18}.Release|Any CPU.Build.0 = Release|Any CPU - {BA43ED8E-04B3-4F04-B828-EC5D7684FEED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA43ED8E-04B3-4F04-B828-EC5D7684FEED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA43ED8E-04B3-4F04-B828-EC5D7684FEED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA43ED8E-04B3-4F04-B828-EC5D7684FEED}.Release|Any CPU.Build.0 = Release|Any CPU - {4E8210DE-33A6-42B0-89F9-ED84A8AC197C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {44B88EFA-1500-4005-AF27-A9BCB9DD79C1} = {7495B75C-C415-4CED-A4D9-6DE4DE9B0A04} - {C77438A0-7745-4F4F-BB8A-CFFA52DC5E18} = {7495B75C-C415-4CED-A4D9-6DE4DE9B0A04} - {BA43ED8E-04B3-4F04-B828-EC5D7684FEED} = {7495B75C-C415-4CED-A4D9-6DE4DE9B0A04} - {4E8210DE-33A6-42B0-89F9-ED84A8AC197C} = {8C69E628-6CFC-4F25-8CAC-EC63927C0120} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {019DD8EE-F4C0-4687-AD7C-FFBB0C8B5DA6} - EndGlobalSection -EndGlobal diff --git a/TwitchLib.Client.slnx b/TwitchLib.Client.slnx new file mode 100644 index 00000000..d2aae641 --- /dev/null +++ b/TwitchLib.Client.slnx @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/TwitchLib.Client/Events/NoticeEventArgs.cs b/TwitchLib.Client/Events/NoticeEventArgs.cs new file mode 100644 index 00000000..41a5b0f9 --- /dev/null +++ b/TwitchLib.Client/Events/NoticeEventArgs.cs @@ -0,0 +1,26 @@ +namespace TwitchLib.Client.Events; + +/// +/// EventArgs representing a NOTICE. +/// +public class NoticeEventArgs +{ + /// + /// Property representing message send with the NOTICE + /// + public string Message { get; } + + /// + /// Property representing channel bot is connected to. + /// + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public NoticeEventArgs(string channel, string message) + { + Message = message; + Channel = channel; + } +} diff --git a/TwitchLib.Client/Events/OnAnnouncementArgs.cs b/TwitchLib.Client/Events/OnAnnouncementArgs.cs index 739827e8..9bc08219 100644 --- a/TwitchLib.Client/Events/OnAnnouncementArgs.cs +++ b/TwitchLib.Client/Events/OnAnnouncementArgs.cs @@ -14,10 +14,19 @@ public class OnAnnouncementArgs : EventArgs /// /// Property representing the announcement send with the USERNOTICE /// - public Announcement Announcement; + public Announcement Announcement { get; } /// /// Property representing channel bot is connected to. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnAnnouncementArgs(string channel, Announcement announcement) + { + Channel = channel; + Announcement = announcement; + } } } \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnAnonGiftPaidUpgradeArgs.cs b/TwitchLib.Client/Events/OnAnonGiftPaidUpgradeArgs.cs new file mode 100644 index 00000000..ac3e0790 --- /dev/null +++ b/TwitchLib.Client/Events/OnAnonGiftPaidUpgradeArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnAnonGiftPaidUpgradeArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The AnonGiftPaidUpgrade + /// + public AnonGiftPaidUpgrade AnonGiftPaidUpgrade { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnAnonGiftPaidUpgradeArgs(string channel, AnonGiftPaidUpgrade anonGiftPaidUpgrade) + { + Channel = channel; + AnonGiftPaidUpgrade = anonGiftPaidUpgrade; + } + + internal OnAnonGiftPaidUpgradeArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + AnonGiftPaidUpgrade = new(ircMessage); + } +} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnBadHostErrorArgs.cs b/TwitchLib.Client/Events/OnBadHostErrorArgs.cs deleted file mode 100644 index 0f4692e7..00000000 --- a/TwitchLib.Client/Events/OnBadHostErrorArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client hosting failed because of a host error - /// Implements the - /// - /// - /// - public class OnBadHostErrorArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnBadHostRateExceededArgs.cs b/TwitchLib.Client/Events/OnBadHostRateExceededArgs.cs deleted file mode 100644 index 611e5416..00000000 --- a/TwitchLib.Client/Events/OnBadHostRateExceededArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client hosting failed because rate limit for hosting was encountered - /// Implements the - /// - /// - /// - public class OnBadHostRateExceededArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnBadUnhostErrorArgs.cs b/TwitchLib.Client/Events/OnBadUnhostErrorArgs.cs deleted file mode 100644 index 2a46809f..00000000 --- a/TwitchLib.Client/Events/OnBadUnhostErrorArgs.cs +++ /dev/null @@ -1,23 +0,0 @@ - -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client unhosting failed because of a host error - /// Implements the - /// - /// - /// - public class OnBadUnhostErrorArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnBannedArgs.cs b/TwitchLib.Client/Events/OnBannedArgs.cs deleted file mode 100644 index 510e4fcb..00000000 --- a/TwitchLib.Client/Events/OnBannedArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because the bot user is banned. - /// Implements the - /// - /// - /// - public class OnBannedArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnBannedEmailAliasArgs.cs b/TwitchLib.Client/Events/OnBannedEmailAliasArgs.cs deleted file mode 100644 index f36d7a45..00000000 --- a/TwitchLib.Client/Events/OnBannedEmailAliasArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client that the user is banned to chat bcs of an already banned alias with the same Email. - /// Implements the - /// - /// - /// - public class OnBannedEmailAliasArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnBitsBadgeTierArgs.cs b/TwitchLib.Client/Events/OnBitsBadgeTierArgs.cs new file mode 100644 index 00000000..7d1a78f0 --- /dev/null +++ b/TwitchLib.Client/Events/OnBitsBadgeTierArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnBitsBadgeTierArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The BitsBadgeTier + /// + public BitsBadgeTier BitsBadgeTier { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnBitsBadgeTierArgs(string channel, BitsBadgeTier bitsBadgeTier) + { + Channel = channel; + BitsBadgeTier = bitsBadgeTier; + } + + internal OnBitsBadgeTierArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + BitsBadgeTier = new(ircMessage); + } +} diff --git a/TwitchLib.Client/Events/OnChannelStateChangedArgs.cs b/TwitchLib.Client/Events/OnChannelStateChangedArgs.cs index c976ef6d..23113343 100644 --- a/TwitchLib.Client/Events/OnChannelStateChangedArgs.cs +++ b/TwitchLib.Client/Events/OnChannelStateChangedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,10 +13,19 @@ public class OnChannelStateChangedArgs : EventArgs /// /// Property representing the current channel state. /// - public ChannelState ChannelState; + public ChannelState ChannelState { get; } /// /// Property representing the channel received state from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnChannelStateChangedArgs(string channel, ChannelState channelState) + { + Channel = channel; + ChannelState = channelState; + } } } diff --git a/TwitchLib.Client/Events/OnChatClearedArgs.cs b/TwitchLib.Client/Events/OnChatClearedArgs.cs index 61820e60..1c2c8e02 100644 --- a/TwitchLib.Client/Events/OnChatClearedArgs.cs +++ b/TwitchLib.Client/Events/OnChatClearedArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing a cleared chat event. @@ -14,5 +12,13 @@ public class OnChatClearedArgs : EventArgs /// Channel that had chat cleared event. /// public string Channel; + + /// + /// Initializes a new instance of the class. + /// + public OnChatClearedArgs(string channel) + { + Channel = channel; + } } } diff --git a/TwitchLib.Client/Events/OnChatColorChangedArgs.cs b/TwitchLib.Client/Events/OnChatColorChangedArgs.cs deleted file mode 100644 index 044d1c26..00000000 --- a/TwitchLib.Client/Events/OnChatColorChangedArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a successful chat color change request. - /// Implements the - /// - /// - /// - public class OnChatColorChangedArgs : EventArgs - { - /// - /// Property reprenting the channel the event was received in. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnChatCommandReceivedArgs.cs b/TwitchLib.Client/Events/OnChatCommandReceivedArgs.cs index ecc376dc..3793ce0a 100644 --- a/TwitchLib.Client/Events/OnChatCommandReceivedArgs.cs +++ b/TwitchLib.Client/Events/OnChatCommandReceivedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -9,12 +8,19 @@ namespace TwitchLib.Client.Events ///
/// /// - public class OnChatCommandReceivedArgs : EventArgs + public class OnChatCommandReceivedArgs : OnMessageReceivedArgs { /// - /// The command - /// /// Property representing received command. - public ChatCommand Command; + ///
+ public CommandInfo Command { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnChatCommandReceivedArgs(ChatMessage message, CommandInfo commandInfo) : base(message) + { + Command = commandInfo; + } } -} +} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnCommunityPayForwardArgs.cs b/TwitchLib.Client/Events/OnCommunityPayForwardArgs.cs new file mode 100644 index 00000000..d99a8975 --- /dev/null +++ b/TwitchLib.Client/Events/OnCommunityPayForwardArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnCommunityPayForwardArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The CommunityPayForward + /// + public CommunityPayForward CommunityPayForward { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnCommunityPayForwardArgs(string channel, CommunityPayForward communityPayForward) + { + Channel = channel; + CommunityPayForward = communityPayForward; + } + + internal OnCommunityPayForwardArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + CommunityPayForward = new(ircMessage); + } +} diff --git a/TwitchLib.Client/Events/OnCommunitySubscriptionArgs.cs b/TwitchLib.Client/Events/OnCommunitySubscriptionArgs.cs index f26aa4d8..c0d6767a 100644 --- a/TwitchLib.Client/Events/OnCommunitySubscriptionArgs.cs +++ b/TwitchLib.Client/Events/OnCommunitySubscriptionArgs.cs @@ -14,10 +14,19 @@ public class OnCommunitySubscriptionArgs : EventArgs /// /// Property representing the information of the community subscription. /// - public CommunitySubscription GiftedSubscription; + public CommunitySubscription GiftedSubscription { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnCommunitySubscriptionArgs(string channel, CommunitySubscription giftedSubscription) + { + Channel = channel; + GiftedSubscription = giftedSubscription; + } } } diff --git a/TwitchLib.Client/Events/OnConnectedArgs.cs b/TwitchLib.Client/Events/OnConnectedArgs.cs deleted file mode 100644 index 0ccf4685..00000000 --- a/TwitchLib.Client/Events/OnConnectedArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing on connected event. - /// Implements the - /// - /// - /// - public class OnConnectedArgs : EventArgs - { - /// - /// Property representing bot username. - /// - public string BotUsername; - /// - /// Property representing connected channel. - /// - public string AutoJoinChannel; - } -} diff --git a/TwitchLib.Client/Events/OnConnectedEventArgs.cs b/TwitchLib.Client/Events/OnConnectedEventArgs.cs new file mode 100644 index 00000000..0482ee4d --- /dev/null +++ b/TwitchLib.Client/Events/OnConnectedEventArgs.cs @@ -0,0 +1,21 @@ +namespace TwitchLib.Client.Events; + +/// +/// Args representing on connected event. +/// Implements the +/// +public class OnConnectedEventArgs : EventArgs +{ + /// + /// Property representing bot username. + /// + public string BotUsername { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnConnectedEventArgs(string botUsername) + { + BotUsername = botUsername; + } +} diff --git a/TwitchLib.Client/Events/OnConnectionErrorArgs.cs b/TwitchLib.Client/Events/OnConnectionErrorArgs.cs index ea3c8ec2..6f259df6 100644 --- a/TwitchLib.Client/Events/OnConnectionErrorArgs.cs +++ b/TwitchLib.Client/Events/OnConnectionErrorArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,10 +13,19 @@ public class OnConnectionErrorArgs : EventArgs /// /// The error /// - public ErrorEvent Error; + public ErrorEvent Error { get; } /// /// Username of the bot that suffered connection error. /// - public string BotUsername; + public string BotUsername { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnConnectionErrorArgs(string botUsername, ErrorEvent error) + { + BotUsername = botUsername; + Error = error; + } } } diff --git a/TwitchLib.Client/Events/OnContinuedGiftedSubscriptionArgs.cs b/TwitchLib.Client/Events/OnContinuedGiftedSubscriptionArgs.cs index 11c0dd8c..875ddbd7 100644 --- a/TwitchLib.Client/Events/OnContinuedGiftedSubscriptionArgs.cs +++ b/TwitchLib.Client/Events/OnContinuedGiftedSubscriptionArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -13,10 +12,19 @@ public class OnContinuedGiftedSubscriptionArgs : EventArgs /// /// Property representing the information of the subscription that was originally gifted, and is now continued by the user. /// - public ContinuedGiftedSubscription ContinuedGiftedSubscription; + public ContinuedGiftedSubscription ContinuedGiftedSubscription { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnContinuedGiftedSubscriptionArgs(string channel, ContinuedGiftedSubscription continuedGiftedSubscription) + { + Channel = channel; + ContinuedGiftedSubscription = continuedGiftedSubscription; + } } } \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnDisconnectedArgs.cs b/TwitchLib.Client/Events/OnDisconnectedArgs.cs index f57aee9d..3abb0dfb 100644 --- a/TwitchLib.Client/Events/OnDisconnectedArgs.cs +++ b/TwitchLib.Client/Events/OnDisconnectedArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing client disconnect event. @@ -13,6 +11,14 @@ public class OnDisconnectedArgs : EventArgs /// /// Username of the bot that was disconnected. /// - public string BotUsername; + public string BotUsername { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnDisconnectedArgs(string botUsername) + { + BotUsername = botUsername; + } } } diff --git a/TwitchLib.Client/Events/OnDuplicateArgs.cs b/TwitchLib.Client/Events/OnDuplicateArgs.cs deleted file mode 100644 index 35602a24..00000000 --- a/TwitchLib.Client/Events/OnDuplicateArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client that duplicate messages are not allowed. - /// Implements the - /// - /// - /// - public class OnDuplicateArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnEmoteOnlyArgs.cs b/TwitchLib.Client/Events/OnEmoteOnlyArgs.cs deleted file mode 100644 index d57bcb63..00000000 --- a/TwitchLib.Client/Events/OnEmoteOnlyArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because its emote only mode. - /// Implements the - /// - /// - /// - public class OnEmoteOnlyArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnExistingUsersDetectedArgs.cs b/TwitchLib.Client/Events/OnExistingUsersDetectedArgs.cs index 1f9b2b83..8af1c3ec 100644 --- a/TwitchLib.Client/Events/OnExistingUsersDetectedArgs.cs +++ b/TwitchLib.Client/Events/OnExistingUsersDetectedArgs.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing existing user(s) detected event. @@ -14,10 +11,19 @@ public class OnExistingUsersDetectedArgs : EventArgs /// /// Property representing string list of existing users. /// - public List Users; + public List Users { get; } /// /// Property representing channel bot is connected to. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnExistingUsersDetectedArgs(string channel, List users) + { + Channel = channel; + Users = users; + } } } diff --git a/TwitchLib.Client/Events/OnFailureToReceiveJoinConfirmationArgs.cs b/TwitchLib.Client/Events/OnFailureToReceiveJoinConfirmationArgs.cs index 423a3b90..067c0299 100644 --- a/TwitchLib.Client/Events/OnFailureToReceiveJoinConfirmationArgs.cs +++ b/TwitchLib.Client/Events/OnFailureToReceiveJoinConfirmationArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Exceptions; +using TwitchLib.Client.Exceptions; namespace TwitchLib.Client.Events { @@ -14,5 +13,13 @@ public class OnFailureToReceiveJoinConfirmationArgs : EventArgs /// The exception /// public FailureToReceiveJoinConfirmationException Exception; + + /// + /// Initializes a new instance of the class. + /// + public OnFailureToReceiveJoinConfirmationArgs(FailureToReceiveJoinConfirmationException exception) + { + Exception = exception; + } } } diff --git a/TwitchLib.Client/Events/OnFollowersOnlyArgs.cs b/TwitchLib.Client/Events/OnFollowersOnlyArgs.cs deleted file mode 100644 index 4565796c..00000000 --- a/TwitchLib.Client/Events/OnFollowersOnlyArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because its followers only mode. - /// Implements the - /// - /// - /// - public class OnFollowersOnlyArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnGiftedSubscriptionArgs.cs b/TwitchLib.Client/Events/OnGiftedSubscriptionArgs.cs index ad56a0a8..1566399b 100644 --- a/TwitchLib.Client/Events/OnGiftedSubscriptionArgs.cs +++ b/TwitchLib.Client/Events/OnGiftedSubscriptionArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -13,10 +12,19 @@ public class OnGiftedSubscriptionArgs : EventArgs /// /// Property representing the information of the gifted subscription. /// - public GiftedSubscription GiftedSubscription; + public GiftedSubscription GiftedSubscription { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnGiftedSubscriptionArgs(string channel, GiftedSubscription giftedSubscription) + { + Channel = channel; + GiftedSubscription = giftedSubscription; + } } } diff --git a/TwitchLib.Client/Events/OnIncorrectLoginArgs.cs b/TwitchLib.Client/Events/OnIncorrectLoginArgs.cs index e77f85db..31db4816 100644 --- a/TwitchLib.Client/Events/OnIncorrectLoginArgs.cs +++ b/TwitchLib.Client/Events/OnIncorrectLoginArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Exceptions; +using TwitchLib.Client.Exceptions; namespace TwitchLib.Client.Events { @@ -15,5 +14,13 @@ public class OnIncorrectLoginArgs : EventArgs /// Property representing exception object. /// public ErrorLoggingInException Exception; + + /// + /// Initializes a new instance of the class. + /// + public OnIncorrectLoginArgs(ErrorLoggingInException exception) + { + Exception = exception; + } } } diff --git a/TwitchLib.Client/Events/OnJoinedChannelArgs.cs b/TwitchLib.Client/Events/OnJoinedChannelArgs.cs index 86de7e5a..c3fd9daa 100644 --- a/TwitchLib.Client/Events/OnJoinedChannelArgs.cs +++ b/TwitchLib.Client/Events/OnJoinedChannelArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing on channel joined event. @@ -13,10 +11,19 @@ public class OnJoinedChannelArgs : EventArgs /// /// Property representing bot username. /// - public string BotUsername; + public string BotUsername { get; } /// /// Property representing the channel that was joined. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnJoinedChannelArgs(string channel, string botUsername) + { + Channel = channel; + BotUsername = botUsername; + } } } diff --git a/TwitchLib.Client/Events/OnLeftChannelArgs.cs b/TwitchLib.Client/Events/OnLeftChannelArgs.cs index 627fe1ad..f06475e0 100644 --- a/TwitchLib.Client/Events/OnLeftChannelArgs.cs +++ b/TwitchLib.Client/Events/OnLeftChannelArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing the client left a channel event. @@ -13,10 +11,19 @@ public class OnLeftChannelArgs : EventArgs /// /// The username of the bot that left the channel. /// - public string BotUsername; + public string BotUsername { get; } /// /// Channel that bot just left (parted). /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnLeftChannelArgs(string channel, string botUsername) + { + Channel = channel; + BotUsername = botUsername; + } } } diff --git a/TwitchLib.Client/Events/OnLogArgs.cs b/TwitchLib.Client/Events/OnLogArgs.cs deleted file mode 100644 index e5d4fef8..00000000 --- a/TwitchLib.Client/Events/OnLogArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Class OnLogArgs. - /// Implements the - /// - /// - public class OnLogArgs : EventArgs - { - /// - /// The bot username - /// - public string BotUsername; - /// - /// The data - /// - public string Data; - /// - /// The date time - /// - public DateTime DateTime; - } -} diff --git a/TwitchLib.Client/Events/OnMessageClearedArgs.cs b/TwitchLib.Client/Events/OnMessageClearedArgs.cs index bcccb26e..b0e1ac5f 100644 --- a/TwitchLib.Client/Events/OnMessageClearedArgs.cs +++ b/TwitchLib.Client/Events/OnMessageClearedArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing a cleared message event. @@ -13,21 +11,32 @@ public class OnMessageClearedArgs : EventArgs /// /// Channel that had message cleared event. /// - public string Channel; + public string Channel { get; } /// /// Message contents that received clear message /// - public string Message; + public string Message { get; } /// /// Message ID representing the message that was cleared /// - public string TargetMessageId; + public string TargetMessageId { get; } /// /// Timestamp of when message was sent /// - public string TmiSentTs; + public DateTimeOffset TmiSent { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnMessageClearedArgs(string channel, string message, string targetMessageId, DateTimeOffset tmiSent) + { + Channel = channel; + Message = message; + TargetMessageId = targetMessageId; + TmiSent = tmiSent; + } } } diff --git a/TwitchLib.Client/Events/OnMessageReceivedArgs.cs b/TwitchLib.Client/Events/OnMessageReceivedArgs.cs index bbaa5ba0..244f3f58 100644 --- a/TwitchLib.Client/Events/OnMessageReceivedArgs.cs +++ b/TwitchLib.Client/Events/OnMessageReceivedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnMessageReceivedArgs : EventArgs /// /// Property representing received chat message. /// - public ChatMessage ChatMessage; + public ChatMessage ChatMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnMessageReceivedArgs(ChatMessage message) + { + ChatMessage = message; + } } } diff --git a/TwitchLib.Client/Events/OnMessageSentArgs.cs b/TwitchLib.Client/Events/OnMessageSentArgs.cs index 058db084..51819cbe 100644 --- a/TwitchLib.Client/Events/OnMessageSentArgs.cs +++ b/TwitchLib.Client/Events/OnMessageSentArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnMessageSentArgs : EventArgs /// /// Property representing a chat message that was just sent (check null on properties before using). /// - public SentMessage SentMessage; + public SentMessage SentMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnMessageSentArgs(SentMessage sentMessage) + { + SentMessage = sentMessage; + } } } diff --git a/TwitchLib.Client/Events/OnMessageThrottledArgs.cs b/TwitchLib.Client/Events/OnMessageThrottledArgs.cs new file mode 100644 index 00000000..05669f06 --- /dev/null +++ b/TwitchLib.Client/Events/OnMessageThrottledArgs.cs @@ -0,0 +1,34 @@ +using System; +using System.Text; +using TwitchLib.Client.Models; + +namespace TwitchLib.Client.Events +{ + public class OnMessageThrottledArgs + { + public string Reason { get; } + public OutboundChatMessage ItemNotSent { get; } + public TimeSpan Period { get; } + public uint AllowedInPeriod { get;} + + public OnMessageThrottledArgs(string reason, OutboundChatMessage itemNotSent, TimeSpan period, uint allowedInPeriod) + { + Reason = reason; + ItemNotSent = itemNotSent; + Period = period; + AllowedInPeriod = allowedInPeriod; + } + + public override string ToString() + { + var builder = new StringBuilder(); + + builder.AppendLine($"{nameof(Reason)}: {Reason}"); + builder.AppendLine($"{nameof(ItemNotSent)}: {ItemNotSent}"); + builder.AppendLine($"{nameof(Period)}: {Period}"); + builder.AppendLine($"{nameof(AllowedInPeriod)}: {AllowedInPeriod}"); + + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnModeratorJoinedArgs.cs b/TwitchLib.Client/Events/OnModeratorJoinedArgs.cs deleted file mode 100644 index b12d8ae7..00000000 --- a/TwitchLib.Client/Events/OnModeratorJoinedArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing moderator joined event. - /// Implements the - /// - /// - /// - public class OnModeratorJoinedArgs : EventArgs - { - /// - /// Property representing username of joined moderator. - /// - public string Username; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnModeratorLeftArgs.cs b/TwitchLib.Client/Events/OnModeratorLeftArgs.cs deleted file mode 100644 index ad69554c..00000000 --- a/TwitchLib.Client/Events/OnModeratorLeftArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing moderator leave event. - /// Implements the - /// - /// - /// - public class OnModeratorLeftArgs : EventArgs - { - /// - /// Property representing username of moderator that left.. - /// - public string Username; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnModeratorsReceivedArgs.cs b/TwitchLib.Client/Events/OnModeratorsReceivedArgs.cs deleted file mode 100644 index 4d47afd6..00000000 --- a/TwitchLib.Client/Events/OnModeratorsReceivedArgs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a list of moderators received from chat. - /// Implements the - /// - /// - /// - public class OnModeratorsReceivedArgs : EventArgs - { - /// - /// Property representing the channel the moderator list came from. - /// - public string Channel; - /// - /// Property representing the list of moderators. - /// - public List Moderators; - } -} diff --git a/TwitchLib.Client/Events/OnNewSubscriberArgs.cs b/TwitchLib.Client/Events/OnNewSubscriberArgs.cs index dc92c0ed..515d6527 100644 --- a/TwitchLib.Client/Events/OnNewSubscriberArgs.cs +++ b/TwitchLib.Client/Events/OnNewSubscriberArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,10 +13,19 @@ public class OnNewSubscriberArgs : EventArgs /// /// Property representing subscriber object. /// - public Subscriber Subscriber; + public Subscriber Subscriber { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnNewSubscriberArgs(string channel, Subscriber subscriber) + { + Channel = channel; + Subscriber = subscriber; + } } } diff --git a/TwitchLib.Client/Events/OnNowHostingArgs.cs b/TwitchLib.Client/Events/OnNowHostingArgs.cs deleted file mode 100644 index a9e326bb..00000000 --- a/TwitchLib.Client/Events/OnNowHostingArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing the detected hosted channel. - /// Implements the - /// - /// - /// - public class OnNowHostingArgs : EventArgs - { - /// - /// Property the channel that received the event. - /// - public string Channel; - /// - /// Property representing channel that is being hosted. - /// - public string HostedChannel; - } -} diff --git a/TwitchLib.Client/Events/OnPrimePaidSubscriberArgs.cs b/TwitchLib.Client/Events/OnPrimePaidSubscriberArgs.cs index 4d8b2ad3..342b1469 100644 --- a/TwitchLib.Client/Events/OnPrimePaidSubscriberArgs.cs +++ b/TwitchLib.Client/Events/OnPrimePaidSubscriberArgs.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -16,10 +13,19 @@ public class OnPrimePaidSubscriberArgs : EventArgs /// /// Property representing prime gaming -> paid subscriber object. /// - public PrimePaidSubscriber PrimePaidSubscriber; + public PrimePaidSubscriber PrimePaidSubscriber { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnPrimePaidSubscriberArgs(string channel, PrimePaidSubscriber primePaidSubscriber) + { + Channel = channel; + PrimePaidSubscriber = primePaidSubscriber; + } } } diff --git a/TwitchLib.Client/Events/OnR9kModeArgs.cs b/TwitchLib.Client/Events/OnR9kModeArgs.cs deleted file mode 100644 index 846c4e5d..00000000 --- a/TwitchLib.Client/Events/OnR9kModeArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because the channel is in r9k mode - /// Implements the - /// - /// - /// - public class OnR9kModeArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnRaidNotificationArgs.cs b/TwitchLib.Client/Events/OnRaidNotificationArgs.cs index 3fee7492..e71bb0ae 100644 --- a/TwitchLib.Client/Events/OnRaidNotificationArgs.cs +++ b/TwitchLib.Client/Events/OnRaidNotificationArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -13,10 +12,19 @@ public class OnRaidNotificationArgs : EventArgs /// /// The raid notification /// - public RaidNotification RaidNotification; + public RaidNotification RaidNotification { get; } /// /// The channel /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnRaidNotificationArgs(string channel, RaidNotification raidNotification) + { + Channel = channel; + RaidNotification = raidNotification; + } } } diff --git a/TwitchLib.Client/Events/OnRateLimitArgs.cs b/TwitchLib.Client/Events/OnRateLimitArgs.cs deleted file mode 100644 index 3d182324..00000000 --- a/TwitchLib.Client/Events/OnRateLimitArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client that a rate limit has been hit. - /// Implements the - /// - /// - /// - public class OnRateLimitArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnReSubscriberArgs.cs b/TwitchLib.Client/Events/OnReSubscriberArgs.cs index 2f14828b..f4be60a1 100644 --- a/TwitchLib.Client/Events/OnReSubscriberArgs.cs +++ b/TwitchLib.Client/Events/OnReSubscriberArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,10 +13,19 @@ public class OnReSubscriberArgs : EventArgs /// /// Property representing resubscriber object. /// - public ReSubscriber ReSubscriber; + public ReSubscriber ReSubscriber { get; } /// /// Property representing the Twitch channel this event fired from. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnReSubscriberArgs(string channel, ReSubscriber reSubscriber) + { + Channel = channel; + ReSubscriber = reSubscriber; + } } } diff --git a/TwitchLib.Client/Events/OnRequiresVerifiedEmailArgs.cs b/TwitchLib.Client/Events/OnRequiresVerifiedEmailArgs.cs deleted file mode 100644 index bf85fa47..00000000 --- a/TwitchLib.Client/Events/OnRequiresVerifiedEmailArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client that a verified email is required to chat. - /// Implements the - /// - /// - /// - public class OnRequiresVerifiedEmailArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnRequiresVerifiedPhoneNumberArgs.cs b/TwitchLib.Client/Events/OnRequiresVerifiedPhoneNumberArgs.cs deleted file mode 100644 index 387b8f05..00000000 --- a/TwitchLib.Client/Events/OnRequiresVerifiedPhoneNumberArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client that a verified phone number is required to chat. - /// Implements the - /// - /// - /// - public class OnRequiresVerifiedPhoneNumberArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnRitualArgs.cs b/TwitchLib.Client/Events/OnRitualArgs.cs new file mode 100644 index 00000000..6c91b303 --- /dev/null +++ b/TwitchLib.Client/Events/OnRitualArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnRitualArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The ritual + /// + public Ritual Ritual { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnRitualArgs(string channel, Ritual ritual) + { + Channel = channel; + Ritual = ritual; + } + + internal OnRitualArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + Ritual = new(ircMessage); + } +} diff --git a/TwitchLib.Client/Events/OnSendReceiveDataArgs.cs b/TwitchLib.Client/Events/OnSendReceiveDataArgs.cs index c3169b6e..1dfb2ce9 100644 --- a/TwitchLib.Client/Events/OnSendReceiveDataArgs.cs +++ b/TwitchLib.Client/Events/OnSendReceiveDataArgs.cs @@ -1,4 +1,4 @@ -using System; +using TwitchLib.Client.Enums; namespace TwitchLib.Client.Events { @@ -13,10 +13,19 @@ public class OnSendReceiveDataArgs : EventArgs /// /// Property representing the direction of the data. /// - public Enums.SendReceiveDirection Direction; + public Enums.SendReceiveDirection Direction { get; } /// /// Property representing the data that was either sent or received. /// - public string Data; + public string Data { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnSendReceiveDataArgs(SendReceiveDirection direction, string data) + { + Direction = direction; + Data = data; + } } } diff --git a/TwitchLib.Client/Events/OnSlowModeArgs.cs b/TwitchLib.Client/Events/OnSlowModeArgs.cs deleted file mode 100644 index d18e1cc0..00000000 --- a/TwitchLib.Client/Events/OnSlowModeArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because its in slow mode. - /// Implements the - /// - /// - /// - public class OnSlowModeArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnStandardPayForwardArgs.cs b/TwitchLib.Client/Events/OnStandardPayForwardArgs.cs new file mode 100644 index 00000000..67dc554b --- /dev/null +++ b/TwitchLib.Client/Events/OnStandardPayForwardArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnStandardPayForwardArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The StandardPayForward + /// + public StandardPayForward StandardPayForward { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnStandardPayForwardArgs(string channel, StandardPayForward standardPayForward) + { + Channel = channel; + StandardPayForward = standardPayForward; + } + + internal OnStandardPayForwardArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + StandardPayForward = new(ircMessage); + } +} diff --git a/TwitchLib.Client/Events/OnSubsOnlyArgs.cs b/TwitchLib.Client/Events/OnSubsOnlyArgs.cs deleted file mode 100644 index 1163de36..00000000 --- a/TwitchLib.Client/Events/OnSubsOnlyArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because its subs only mode. - /// Implements the - /// - /// - /// - public class OnSubsOnlyArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnSuspendedArgs.cs b/TwitchLib.Client/Events/OnSuspendedArgs.cs deleted file mode 100644 index 94075f31..00000000 --- a/TwitchLib.Client/Events/OnSuspendedArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a NOTICE telling the client a message was not sent because the channel is suspended. - /// Implements the - /// - /// - /// - public class OnSuspendedArgs : EventArgs - { - /// - /// Property representing message send with the NOTICE - /// - public string Message; - /// - /// Property representing channel bot is connected to. - /// - public string Channel; - } -} diff --git a/TwitchLib.Client/Events/OnUnaccountedForArgs.cs b/TwitchLib.Client/Events/OnUnaccountedForArgs.cs index 7f321db0..916ccba0 100644 --- a/TwitchLib.Client/Events/OnUnaccountedForArgs.cs +++ b/TwitchLib.Client/Events/OnUnaccountedForArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Class OnUnaccountedForArgs. @@ -23,11 +21,19 @@ public class OnUnaccountedForArgs : EventArgs /// Gets or sets the bot username. /// /// The bot username. - public string BotUsername { get; set; } // may not be available + public string? BotUsername { get; set; } /// /// Gets or sets the channel. /// /// The channel. - public string Channel { get; set; } // may not be available + public string? Channel { get; set; } + + public OnUnaccountedForArgs(string? botUsername, string? channel, string location, string rawIRC) + { + BotUsername = botUsername; + Channel = channel; + Location = location; + RawIRC = rawIRC; + } } } diff --git a/TwitchLib.Client/Events/OnUnraidNotificationArgs.cs b/TwitchLib.Client/Events/OnUnraidNotificationArgs.cs new file mode 100644 index 00000000..885601b4 --- /dev/null +++ b/TwitchLib.Client/Events/OnUnraidNotificationArgs.cs @@ -0,0 +1,32 @@ +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Events; + +public class OnUnraidNotificationArgs : EventArgs +{ + /// + /// The channel + /// + public string Channel { get; } + + /// + /// The unraid notification + /// + public UnraidNotification RaidNotification { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUnraidNotificationArgs(string channel, UnraidNotification unraidNotification) + { + Channel = channel; + RaidNotification = unraidNotification; + } + + internal OnUnraidNotificationArgs(IrcMessage ircMessage) + { + Channel = ircMessage.Channel; + RaidNotification = new(ircMessage); + } +} diff --git a/TwitchLib.Client/Events/OnUserBannedArgs.cs b/TwitchLib.Client/Events/OnUserBannedArgs.cs index 536f3fb0..7283b2cf 100644 --- a/TwitchLib.Client/Events/OnUserBannedArgs.cs +++ b/TwitchLib.Client/Events/OnUserBannedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnUserBannedArgs : EventArgs /// /// The user ban /// - public UserBan UserBan; + public UserBan UserBan { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserBannedArgs(UserBan userBan) + { + UserBan = userBan; + } } } diff --git a/TwitchLib.Client/Events/OnUserIntroArgs.cs b/TwitchLib.Client/Events/OnUserIntroArgs.cs index 933b082f..575044b4 100644 --- a/TwitchLib.Client/Events/OnUserIntroArgs.cs +++ b/TwitchLib.Client/Events/OnUserIntroArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnUserIntroArgs : EventArgs /// /// Property representing the PRIVMSG /// - public ChatMessage ChatMessage; + public ChatMessage ChatMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserIntroArgs(ChatMessage chatMessage) + { + ChatMessage = chatMessage; + } } } \ No newline at end of file diff --git a/TwitchLib.Client/Events/OnUserJoinedArgs.cs b/TwitchLib.Client/Events/OnUserJoinedArgs.cs index 9048e05f..de1be33a 100644 --- a/TwitchLib.Client/Events/OnUserJoinedArgs.cs +++ b/TwitchLib.Client/Events/OnUserJoinedArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing viewer joined event. @@ -13,10 +11,19 @@ public class OnUserJoinedArgs : EventArgs /// /// Property representing username of joined viewer. /// - public string Username; + public string Username { get; } /// /// Property representing channel bot is connected to. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserJoinedArgs(string channel, string username) + { + Channel = channel; + Username = username; + } } } diff --git a/TwitchLib.Client/Events/OnUserLeftArgs.cs b/TwitchLib.Client/Events/OnUserLeftArgs.cs index 99b47269..9f18f3f6 100644 --- a/TwitchLib.Client/Events/OnUserLeftArgs.cs +++ b/TwitchLib.Client/Events/OnUserLeftArgs.cs @@ -1,6 +1,4 @@ -using System; - -namespace TwitchLib.Client.Events +namespace TwitchLib.Client.Events { /// /// Args representing viewer left event. @@ -13,10 +11,19 @@ public class OnUserLeftArgs : EventArgs /// /// Property representing username of user that left. /// - public string Username; + public string Username { get; } /// /// Property representing channel bot is connected to. /// - public string Channel; + public string Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserLeftArgs(string channel, string username) + { + Channel = channel; + Username = username; + } } } diff --git a/TwitchLib.Client/Events/OnUserStateChangedArgs.cs b/TwitchLib.Client/Events/OnUserStateChangedArgs.cs index d76706c0..4807e906 100644 --- a/TwitchLib.Client/Events/OnUserStateChangedArgs.cs +++ b/TwitchLib.Client/Events/OnUserStateChangedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnUserStateChangedArgs : EventArgs /// /// Property representing user state object. /// - public UserState UserState; + public UserState UserState { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserStateChangedArgs(UserState userState) + { + UserState = userState; + } } } diff --git a/TwitchLib.Client/Events/OnUserTimedoutArgs.cs b/TwitchLib.Client/Events/OnUserTimedoutArgs.cs index 89d4e22d..888a63df 100644 --- a/TwitchLib.Client/Events/OnUserTimedoutArgs.cs +++ b/TwitchLib.Client/Events/OnUserTimedoutArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnUserTimedoutArgs : EventArgs /// /// The user timeout /// - public UserTimeout UserTimeout; + public UserTimeout UserTimeout { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnUserTimedoutArgs(UserTimeout userTimeout) + { + UserTimeout = userTimeout; + } } } diff --git a/TwitchLib.Client/Events/OnVIPsReceivedArgs.cs b/TwitchLib.Client/Events/OnVIPsReceivedArgs.cs deleted file mode 100644 index 20405890..00000000 --- a/TwitchLib.Client/Events/OnVIPsReceivedArgs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing a list of VIPs received from chat. - /// Implements the - /// - /// - /// - public class OnVIPsReceivedArgs : EventArgs - { - /// - /// Property representing the channel the VIPs list came from. - /// - public string Channel; - /// - /// Property representing the list of VIPs. - /// - public List VIPs; - } -} diff --git a/TwitchLib.Client/Events/OnWhisperCommandReceivedArgs.cs b/TwitchLib.Client/Events/OnWhisperCommandReceivedArgs.cs index e22cc18a..657227f2 100644 --- a/TwitchLib.Client/Events/OnWhisperCommandReceivedArgs.cs +++ b/TwitchLib.Client/Events/OnWhisperCommandReceivedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -9,11 +8,19 @@ namespace TwitchLib.Client.Events /// /// /// - public class OnWhisperCommandReceivedArgs : EventArgs + public class OnWhisperCommandReceivedArgs : OnWhisperReceivedArgs { /// /// Property representing received command. /// - public WhisperCommand Command; + public CommandInfo Command { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnWhisperCommandReceivedArgs(WhisperMessage message, CommandInfo commandInfo) : base(message) + { + Command = commandInfo; + } } } diff --git a/TwitchLib.Client/Events/OnWhisperReceivedArgs.cs b/TwitchLib.Client/Events/OnWhisperReceivedArgs.cs index 1638b5e6..3a374c02 100644 --- a/TwitchLib.Client/Events/OnWhisperReceivedArgs.cs +++ b/TwitchLib.Client/Events/OnWhisperReceivedArgs.cs @@ -1,5 +1,4 @@ -using System; -using TwitchLib.Client.Models; +using TwitchLib.Client.Models; namespace TwitchLib.Client.Events { @@ -14,6 +13,14 @@ public class OnWhisperReceivedArgs : EventArgs /// /// The whisper message /// - public WhisperMessage WhisperMessage; + public WhisperMessage WhisperMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + public OnWhisperReceivedArgs(WhisperMessage message) + { + WhisperMessage = message; + } } } diff --git a/TwitchLib.Client/Events/OnWhisperSentArgs.cs b/TwitchLib.Client/Events/OnWhisperSentArgs.cs deleted file mode 100644 index 2efeba7a..00000000 --- a/TwitchLib.Client/Events/OnWhisperSentArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace TwitchLib.Client.Events -{ - /// - /// Args representing whisper sent event. - /// Implements the - /// - /// - /// - public class OnWhisperSentArgs : EventArgs - { - /// - /// Property representing username of bot. - /// - public string Username; - /// - /// Property representing receiver of the whisper. - /// - public string Receiver; - /// - /// Property representing sent message contents. - /// - public string Message; - } -} diff --git a/TwitchLib.Client/Events/SlowModeNoticeEventArgs.cs b/TwitchLib.Client/Events/SlowModeNoticeEventArgs.cs new file mode 100644 index 00000000..e0ecd39a --- /dev/null +++ b/TwitchLib.Client/Events/SlowModeNoticeEventArgs.cs @@ -0,0 +1,14 @@ +namespace TwitchLib.Client.Events; + +public class SlowModeNoticeEventArgs : NoticeEventArgs +{ + public bool IsActive { get; } + + /// + /// Initializes a new instance of the class. + /// + public SlowModeNoticeEventArgs(string channel, string message, bool isActive) : base(channel, message) + { + IsActive = isActive; + } +} \ No newline at end of file diff --git a/TwitchLib.Client/Exceptions/BadListenException.cs b/TwitchLib.Client/Exceptions/BadListenException.cs deleted file mode 100644 index c99fe193..00000000 --- a/TwitchLib.Client/Exceptions/BadListenException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace TwitchLib.Client.Exceptions -{ - /// - /// Exception thrown when an event is subscribed to when it shouldn't be. - /// Implements the - /// - /// - /// - public class BadListenException : Exception - { - /// - /// Exception constructor - /// - /// Name of the event. - /// The additional details. - /// - public BadListenException(string eventName, string additionalDetails = "") - : base(string.IsNullOrEmpty(additionalDetails) - ? $"You are listening to event '{eventName}', which is not currently allowed. See details: {additionalDetails}" - : $"You are listening to event '{eventName}', which is not currently allowed.") - { - } - } -} diff --git a/TwitchLib.Client/Exceptions/EventNotHandledException.cs b/TwitchLib.Client/Exceptions/EventNotHandledException.cs deleted file mode 100644 index 8ad49995..00000000 --- a/TwitchLib.Client/Exceptions/EventNotHandledException.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace TwitchLib.Client.Exceptions -{ - /// - /// Exception thrown when an event that is not handled is required to be handled. - /// Implements the - /// - /// - /// - public class EventNotHandled : Exception - { - /// - /// Exception constructor - /// - /// Name of the event. - /// The additional details. - /// - public EventNotHandled(string eventName, string additionalDetails = "") - : base(string.IsNullOrEmpty(additionalDetails) - ? $"To use this call, you must handle/subscribe to event: {eventName}" - : $"To use this call, you must handle/subscribe to event: {eventName}, additional details: {additionalDetails}") - { - } - } -} diff --git a/TwitchLib.Client/Exceptions/FailureToReceiveJoinConfirmationException.cs b/TwitchLib.Client/Exceptions/FailureToReceiveJoinConfirmationException.cs index 66a54ebe..d975e8a6 100644 --- a/TwitchLib.Client/Exceptions/FailureToReceiveJoinConfirmationException.cs +++ b/TwitchLib.Client/Exceptions/FailureToReceiveJoinConfirmationException.cs @@ -14,14 +14,14 @@ public class FailureToReceiveJoinConfirmationException /// Extra details regarding this exception (not always set) /// /// The details. - public string Details { get; protected set; } + public string? Details { get; protected set; } /// /// Exception construtor. /// /// The channel. /// The details. - public FailureToReceiveJoinConfirmationException(string channel, string details = null) + public FailureToReceiveJoinConfirmationException(string channel, string? details = null) { Channel = channel; Details = details; diff --git a/TwitchLib.Client/Exceptions/InvalidParameterException.cs b/TwitchLib.Client/Exceptions/InvalidParameterException.cs deleted file mode 100644 index 0308d24d..00000000 --- a/TwitchLib.Client/Exceptions/InvalidParameterException.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace TwitchLib.Client.Exceptions -{ - /// - /// Exception representing bad information being provided to function/method. - /// Implements the - /// - /// - /// - public class InvalidParameterException : Exception - { - /// - /// Username that had the exception. - /// - /// The username. - public string Username { get; protected set; } - - /// - /// Exception construtor. - /// - /// The reasoning. - /// The twitch username. - /// - public InvalidParameterException(string reasoning, string twitchUsername) - : base(reasoning) - { - Username = twitchUsername; - } - } -} diff --git a/TwitchLib.Client/Extensions/AnnoucementExt.cs b/TwitchLib.Client/Extensions/AnnoucementExt.cs deleted file mode 100644 index 362c3a56..00000000 --- a/TwitchLib.Client/Extensions/AnnoucementExt.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing the Announcement functionality in TwitchClient. - /// - public static class AnnoucementExt - { - /// - /// Send an Announcement to a channel using a JoinedChannel - /// - /// Client reference used to identify extension. - /// JoinedChannel object to announce to - /// Message to announce - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.SendChatAnnouncementAsync() instead.")] - public static void Announce(this ITwitchClient client, JoinedChannel channel, string message) - { - client.SendMessage(channel, $".announce {message}"); - } - - /// - /// Send an Announcement to a channel using a string to represent the channel - /// - /// Client reference used to identify extension. - /// Channel in string form to send announce to - /// Message to announce - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.SendChatAnnouncementAsync() instead.")] - public static void Announce(this ITwitchClient client, string channel, string message) - { - client.SendMessage(channel, $".announce {message}"); - } - } -} \ No newline at end of file diff --git a/TwitchLib.Client/Extensions/BanUserExt.cs b/TwitchLib.Client/Extensions/BanUserExt.cs deleted file mode 100644 index cc8cf5a8..00000000 --- a/TwitchLib.Client/Extensions/BanUserExt.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing the ban functionality in TwitchClient. - /// - public static class BanUserExt - { - /// - /// Bans a user in chat using JoinedChannel - /// - /// Client reference used to identify extension. - /// JoinedChannel object to send ban to - /// Viewer name to ban - /// Message to accompany the ban and show the user. - /// Indicates a dryrun (will not send if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.BanUserAsync() instead.")] - public static void BanUser(this ITwitchClient client, JoinedChannel channel, string viewer, string message = "", bool dryRun = false) - { - client.SendMessage(channel, $".ban {viewer} {message}"); - } - - /// - /// Bans a user in chat using a string for the channel - /// - /// Client reference used to identify extension. - /// Channel in string form to send ban to - /// Viewer name to ban - /// Message to accompany the ban and show the user. - /// Indicates a dryrun (will not send if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.BanUserAsync() instead.")] - public static void BanUser(this ITwitchClient client, string channel, string viewer, string message = "", bool dryRun = false) - { - JoinedChannel joinedChannel = client.GetJoinedChannel(channel); - if (joinedChannel != null) - BanUser(client, joinedChannel, viewer, message, dryRun); - } - } -} diff --git a/TwitchLib.Client/Extensions/ChangeChatColorExt.cs b/TwitchLib.Client/Extensions/ChangeChatColorExt.cs deleted file mode 100644 index 3651bd1f..00000000 --- a/TwitchLib.Client/Extensions/ChangeChatColorExt.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing the change chat color functionality in TwitchClient - /// - public static class ChangeChatColorExt - { - /// - /// Sends request to change color of chat name in Twitch chat. - /// - /// Client reference used to identify extension. - /// JoinedChannel object representing which channel to send command to. - /// Enum representing available chat preset colors. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateUserChatColorAsync() instead.")] - public static void ChangeChatColor(this ITwitchClient client, JoinedChannel channel, Enums.ChatColorPresets color) - { - client.SendMessage(channel, $".color {color}"); - } - - /// - /// Sends request to change color of chat name in Twitch chat. - /// - /// Client reference used to identify extension. - /// String representing the channel to send the command to. - /// Enum representing available chat preset colors. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateUserChatColorAsync() instead.")] - public static void ChangeChatColor(this ITwitchClient client, string channel, Enums.ChatColorPresets color) - { - client.SendMessage(channel, $".color {color}"); - } - } -} diff --git a/TwitchLib.Client/Extensions/ClearChatExt.cs b/TwitchLib.Client/Extensions/ClearChatExt.cs deleted file mode 100644 index 3d241134..00000000 --- a/TwitchLib.Client/Extensions/ClearChatExt.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing clear chat functionality in TwitchClient. - /// - public static class ClearChatExt - { - /// - /// Sends request to clear chat (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send clear chat command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void ClearChat(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".clear"); - } - - /// - /// Sends request to clear chat (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// String representation of which channel to send clear chat command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void ClearChat(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".clear"); - } - } -} diff --git a/TwitchLib.Client/Extensions/CommercialExt.cs b/TwitchLib.Client/Extensions/CommercialExt.cs deleted file mode 100644 index e953a45f..00000000 --- a/TwitchLib.Client/Extensions/CommercialExt.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing Commercial functionality in TwitchClient. - /// - public static class CommercialExt - { - /// - /// Sends command to start a commercial of variable length. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the ad to. - /// Enum representing the length of advertisement should be. - /// length - null - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Ads.StartCommercialAsync() instead.")] - public static void StartCommercial(this ITwitchClient client, JoinedChannel channel, Enums.CommercialLength length) - { - switch (length) - { - case Enums.CommercialLength.Seconds30: - client.SendMessage(channel, ".commercial 30"); - break; - case Enums.CommercialLength.Seconds60: - client.SendMessage(channel, ".commercial 60"); - break; - case Enums.CommercialLength.Seconds90: - client.SendMessage(channel, ".commercial 90"); - break; - case Enums.CommercialLength.Seconds120: - client.SendMessage(channel, ".commercial 120"); - break; - case Enums.CommercialLength.Seconds150: - client.SendMessage(channel, ".commercial 150"); - break; - case Enums.CommercialLength.Seconds180: - client.SendMessage(channel, ".commercial 180"); - break; - default: - throw new ArgumentOutOfRangeException(nameof(length), length, null); - } - } - - /// - /// Sends command to start a commercial of variable length. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the ad to. - /// Enum representing the length of advertisement should be. - /// length - null - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Ads.StartCommercialAsync() instead.")] - public static void StartCommercial(this ITwitchClient client, string channel, Enums.CommercialLength length) - { - switch (length) - { - case Enums.CommercialLength.Seconds30: - client.SendMessage(channel, ".commercial 30"); - break; - case Enums.CommercialLength.Seconds60: - client.SendMessage(channel, ".commercial 60"); - break; - case Enums.CommercialLength.Seconds90: - client.SendMessage(channel, ".commercial 90"); - break; - case Enums.CommercialLength.Seconds120: - client.SendMessage(channel, ".commercial 120"); - break; - case Enums.CommercialLength.Seconds150: - client.SendMessage(channel, ".commercial 150"); - break; - case Enums.CommercialLength.Seconds180: - client.SendMessage(channel, ".commercial 180"); - break; - default: - throw new ArgumentOutOfRangeException(nameof(length), length, null); - } - } - } -} diff --git a/TwitchLib.Client/Extensions/DeleteMessageExt.cs b/TwitchLib.Client/Extensions/DeleteMessageExt.cs deleted file mode 100644 index a9eb3aa1..00000000 --- a/TwitchLib.Client/Extensions/DeleteMessageExt.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing delete message functionality in TwitchClient. - /// - public static class DeleteMessageExt - { - /// - /// Sends request to deleete a specific chat message (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send delete message command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void DeleteMessage(this ITwitchClient client, JoinedChannel channel, string messageId) - { - client.SendMessage(channel, $".delete {messageId}"); - } - - /// - /// Sends request to delete a specific chat message (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// String representation of which channel to delete message command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void DeleteMessage(this ITwitchClient client, string channel, string messageId) - { - client.SendMessage(channel, $".delete {messageId}"); - } - - /// - /// Sends request to delete a specific chat message (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send delete message command to. - /// ChatMessage object representing chat message that should be deleted. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void DeleteMessage(this ITwitchClient client, JoinedChannel channel, ChatMessage msg) - { - client.SendMessage(channel, $".delete {msg.Id}"); - } - - /// - /// Sends request to delete a specific chat message (may be ignored by plugins like BTTV) - /// - /// Client reference used to identify extension. - /// String representation of which channel to delete message command to. - /// ChatMessage object representing chat message that should be deleted. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChatMessagesAsync() instead.")] - public static void DeleteMessage(this ITwitchClient client, string channel, ChatMessage msg) - { - client.SendMessage(channel, $".delete {msg.Id}"); - } - } -} diff --git a/TwitchLib.Client/Extensions/DictionaryExtensions.cs b/TwitchLib.Client/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..3ce86ef7 --- /dev/null +++ b/TwitchLib.Client/Extensions/DictionaryExtensions.cs @@ -0,0 +1,12 @@ +namespace TwitchLib.Client.Models.Extensions; + +#if NETSTANDARD2_0 +internal static class DictionaryExtensions +{ + public static TValue GetValueOrDefault(this Dictionary dictionary, TKey key, TValue defaultValue) + { + TValue? value; + return dictionary.TryGetValue(key, out value) ? value : defaultValue; + } +} +#endif diff --git a/TwitchLib.Client/Extensions/EmoteOnlyExt.cs b/TwitchLib.Client/Extensions/EmoteOnlyExt.cs deleted file mode 100644 index 9e8251fc..00000000 --- a/TwitchLib.Client/Extensions/EmoteOnlyExt.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing emote only mode functionality in TwitchClient - /// - public static class EmoteOnlyExt - { - /// - /// Enables emote only chat requirement. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the enable emote only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void EmoteOnlyOn(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".emoteonly"); - } - - /// - /// Enables emote only chat requirement. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the enable emote only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void EmoteOnlyOn(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".emoteonly"); - } - - /// - /// Disables emote only chat requirement. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the disable emote only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void EmoteOnlyOff(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".emoteonlyoff"); - } - - /// - /// Disables emote only chat requirement. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the disable emote only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void EmoteOnlyOff(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".emoteonlyoff"); - } - } -} diff --git a/TwitchLib.Client/Extensions/EventInvocationExt.cs b/TwitchLib.Client/Extensions/EventInvocationExt.cs deleted file mode 100644 index bc872745..00000000 --- a/TwitchLib.Client/Extensions/EventInvocationExt.cs +++ /dev/null @@ -1,686 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using TwitchLib.Client.Enums; -using TwitchLib.Client.Events; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Class EventInvocationExt. - /// - public static class EventInvocationExt - { - - /// - /// Invokes the channel state changed. - /// - /// The client. - /// The channel. - /// if set to true [R9K]. - /// if set to true [rituals]. - /// if set to true [sub only]. - /// The slow mode. - /// if set to true [emote only]. - /// The broadcaster language. - /// The followers only. - /// if set to true [mercury]. - /// The room identifier. - public static void InvokeChannelStateChanged(this TwitchClient client, string channel, bool r9k, bool rituals, - bool subOnly, int slowMode, bool emoteOnly, string broadcasterLanguage, TimeSpan followersOnly, bool mercury, string roomId) - { - ChannelState state = new ChannelState(r9k, rituals, subOnly, slowMode, emoteOnly, broadcasterLanguage, channel, followersOnly, mercury, roomId); - OnChannelStateChangedArgs model = new OnChannelStateChangedArgs() - { - Channel = channel, - ChannelState = state - }; - client.RaiseEvent("OnChannelStateChanged", model); - } - - /// - /// Invokes the chat cleared. - /// - /// The client. - /// The channel. - public static void InvokeChatCleared(this TwitchClient client, string channel) - { - OnChatClearedArgs model = new OnChatClearedArgs() - { - Channel = channel - }; - client.RaiseEvent("OnChatCleared", model); - } - - /// - /// Invokes the chat commands received. - /// - /// The client. - /// The bot username. - /// The user identifier. - /// Name of the user. - /// The display name. - /// The color hexadecimal. - /// The color. - /// The emote set. - /// The message. - /// Type of the user. - /// The channel. - /// The identifier. - /// if set to true [is subscriber]. - /// The subscribed month count. - /// The room identifier. - /// if set to true [is turbo]. - /// if set to true [is moderator]. - /// if set to true [is me]. - /// if set to true [is broadcaster]. - /// if set to true [is VIP]. - /// if set to true [is Partner]. - /// if set to true [is Staff]. - /// The noisy. - /// The raw irc message. - /// The emote replaced message. - /// The badges. - /// The cheer badge. - /// The bits. - /// The bits in dollars. - /// The command text. - /// The arguments as string. - /// The arguments as list. - /// The command identifier. - public static void InvokeChatCommandsReceived(this TwitchClient client, string botUsername, string userId, string userName, string displayName, - string colorHex, Color color, EmoteSet emoteSet, string message, UserType userType, string channel, string id, bool isSubscriber, int subscribedMonthCount, - string roomId, bool isTurbo, bool isModerator, bool isMe, bool isBroadcaster, bool isVip, bool isPartner, bool isStaff, Noisy noisy, string rawIrcMessage, string emoteReplacedMessage, - List> badges, CheerBadge cheerBadge, int bits, double bitsInDollars, string commandText, string argumentsAsString, - List argumentsAsList, char commandIdentifier) - { - ChatMessage msg = new ChatMessage(botUsername, userId, userName, displayName, colorHex, color, emoteSet, message, userType, channel, id, - isSubscriber, subscribedMonthCount, roomId, isTurbo, isModerator, isMe, isBroadcaster, isVip, isPartner, isStaff, noisy, rawIrcMessage, emoteReplacedMessage, - badges, cheerBadge, bits, bitsInDollars); - OnChatCommandReceivedArgs model = new OnChatCommandReceivedArgs() - { - Command = new ChatCommand(msg, commandText, argumentsAsString, argumentsAsList, commandIdentifier) - }; - client.RaiseEvent("OnChatCommandReceived", model); - } - - /// - /// Invokes the connected. - /// - /// The client. - /// The automatic join channel. - /// The bot username. - public static void InvokeConnected(this TwitchClient client, string autoJoinChannel, string botUsername) - { - OnConnectedArgs model = new OnConnectedArgs() - { - AutoJoinChannel = autoJoinChannel, - BotUsername = botUsername - }; - client.RaiseEvent("OnConnected", model); - } - - /// - /// Invokes the connection error. - /// - /// The client. - /// The bot username. - /// The error event. - public static void InvokeConnectionError(this TwitchClient client, string botUsername, ErrorEvent errorEvent) - { - OnConnectionErrorArgs model = new OnConnectionErrorArgs() - { - BotUsername = botUsername, - Error = errorEvent - }; - client.RaiseEvent("OnConnectionError", model); - } - - /// - /// Invokes the disconnected. - /// - /// The client. - /// The bot username. - public static void InvokeDisconnected(this TwitchClient client, string botUsername) - { - OnDisconnectedArgs model = new OnDisconnectedArgs() - { - BotUsername = botUsername - }; - client.RaiseEvent("OnDisconnected", model); - } - - /// - /// Invokes the existing users detected. - /// - /// The client. - /// The channel. - /// The users. - public static void InvokeExistingUsersDetected(this TwitchClient client, string channel, List users) - { - OnExistingUsersDetectedArgs model = new OnExistingUsersDetectedArgs() - { - Channel = channel, - Users = users - }; - client.RaiseEvent("OnExistingUsersDetected", model); - } - - /// - /// Invokes the gifted subscription. - /// - /// The client. - /// The badges. - /// The color. - /// The display name. - /// The emotes. - /// The identifier. - /// The login. - /// if set to true [is moderator]. - /// The MSG identifier. - /// The MSG parameter months. - /// Display name of the MSG parameter recipient. - /// The MSG parameter recipient identifier. - /// Name of the MSG parameter recipient user. - /// Name of the MSG parameter sub plan. - /// The MSG parameter sub plan. - /// The room identifier. - /// if set to true [is subscriber]. - /// The system MSG. - /// The system MSG parsed. - /// The tmi sent ts. - /// if set to true [is turbo]. - /// Type of the user. - /// Id of the user. - public static void InvokeGiftedSubscription(this TwitchClient client, List> badges, List> badgeInfo, string color, string displayName, string emotes, string id, string login, bool isModerator, - string msgId, string msgParamMonths, string msgParamRecipientDisplayName, string msgParamRecipientId, string msgParamRecipientUserName, - string msgParamSubPlanName, string msgMultiMonthGiftDuration, SubscriptionPlan msgParamSubPlan, string roomId, bool isSubscriber, string systemMsg, string systemMsgParsed, - string tmiSentTs, bool isTurbo, UserType userType, string userId) - { - OnGiftedSubscriptionArgs model = new OnGiftedSubscriptionArgs() - { - GiftedSubscription = new GiftedSubscription(badges, badgeInfo, color, displayName, emotes, id, login, isModerator, msgId, msgParamMonths, msgParamRecipientDisplayName, - msgParamRecipientId, msgParamRecipientUserName, msgParamSubPlanName, msgMultiMonthGiftDuration, msgParamSubPlan, roomId, isSubscriber, systemMsg, systemMsgParsed, tmiSentTs, isTurbo, - userType, userId) - }; - client.RaiseEvent("OnGiftedSubscription", model); - } - - /// - /// Invokes the incorrect login. - /// - /// The client. - /// The ex. - public static void InvokeIncorrectLogin(this TwitchClient client, Exceptions.ErrorLoggingInException ex) - { - OnIncorrectLoginArgs model = new OnIncorrectLoginArgs() - { - Exception = ex - }; - client.RaiseEvent("OnIncorrectLogin", model); - } - - /// - /// Invokes the joined channel. - /// - /// The client. - /// The bot username. - /// The channel. - public static void InvokeJoinedChannel(this TwitchClient client, string botUsername, string channel) - { - OnJoinedChannelArgs model = new OnJoinedChannelArgs() - { - BotUsername = botUsername, - Channel = channel - }; - client.RaiseEvent("OnJoinedChannel", model); - } - - /// - /// Invokes the left channel. - /// - /// The client. - /// The bot username. - /// The channel. - public static void InvokeLeftChannel(this TwitchClient client, string botUsername, string channel) - { - OnLeftChannelArgs model = new OnLeftChannelArgs() - { - BotUsername = botUsername, - Channel = channel - }; - client.RaiseEvent("OnLeftChannel", model); - } - - /// - /// Invokes the log. - /// - /// The client. - /// The bot username. - /// The data. - /// The date time. - public static void InvokeLog(this TwitchClient client, string botUsername, string data, DateTime dateTime) - { - OnLogArgs model = new OnLogArgs() - { - BotUsername = botUsername, - Data = data, - DateTime = dateTime - }; - client.RaiseEvent("OnLog", model); - } - - /// - /// Invokes the message received. - /// - /// The client. - /// The bot username. - /// The user identifier. - /// Name of the user. - /// The display name. - /// The color hexadecimal. - /// The color. - /// The emote set. - /// The message. - /// Type of the user. - /// The channel. - /// The identifier. - /// if set to true [is subscriber]. - /// The subscribed month count. - /// The room identifier. - /// if set to true [is turbo]. - /// if set to true [is moderator]. - /// if set to true [is me]. - /// if set to true [is broadcaster]. - /// The noisy. - /// The raw irc message. - /// The emote replaced message. - /// The badges. - /// The cheer badge. - /// The bits. - /// The bits in dollars. - public static void InvokeMessageReceived(this TwitchClient client, string botUsername, string userId, string userName, string displayName, string colorHex, - Color color, EmoteSet emoteSet, string message, UserType userType, string channel, string id, bool isSubscriber, int subscribedMonthCount, string roomId, bool isTurbo, - bool isModerator, bool isMe, bool isBroadcaster, bool isVip, bool isPartner, bool isStaff, Noisy noisy, string rawIrcMessage, string emoteReplacedMessage, List> badges, - CheerBadge cheerBadge, int bits, double bitsInDollars) - { - OnMessageReceivedArgs model = new OnMessageReceivedArgs() - { - ChatMessage = new ChatMessage(botUsername, userId, userName, displayName, colorHex, color, emoteSet, message, userType, channel, id, isSubscriber, - subscribedMonthCount, roomId, isTurbo, isModerator, isMe, isBroadcaster, isVip, isPartner, isStaff, noisy, rawIrcMessage, emoteReplacedMessage, badges, cheerBadge, bits, - bitsInDollars) - }; - client.RaiseEvent("OnMessageReceived", model); - } - - /// - /// Invokes the message sent. - /// - /// The client. - /// The badges. - /// The channel. - /// The color hexadecimal. - /// The display name. - /// The emote set. - /// if set to true [is moderator]. - /// if set to true [is subscriber]. - /// Type of the user. - /// The message. - public static void InvokeMessageSent(this TwitchClient client, List> badges, string channel, string colorHex, - string displayName, string emoteSet, bool isModerator, bool isSubscriber, UserType userType, string message) - { - OnMessageSentArgs model = new OnMessageSentArgs() - { - SentMessage = new SentMessage(badges, channel, colorHex, displayName, emoteSet, isModerator, isSubscriber, userType, message) - }; - client.RaiseEvent("OnMessageSent", model); - } - - /// - /// Invokes the moderator joined. - /// - /// The client. - /// The channel. - /// The username. - public static void InvokeModeratorJoined(this TwitchClient client, string channel, string username) - { - OnModeratorJoinedArgs model = new OnModeratorJoinedArgs() - { - Channel = channel, - Username = username - }; - client.RaiseEvent("OnModeratorJoined", model); - } - - /// - /// Invokes the moderator left. - /// - /// The client. - /// The channel. - /// The username. - public static void InvokeModeratorLeft(this TwitchClient client, string channel, string username) - { - OnModeratorLeftArgs model = new OnModeratorLeftArgs() - { - Channel = channel, - Username = username - }; - client.RaiseEvent("OnModeratorLeft", model); - } - - /// - /// Invokes the moderators received. - /// - /// The client. - /// The channel. - /// The moderators. - public static void InvokeModeratorsReceived(this TwitchClient client, string channel, List moderators) - { - OnModeratorsReceivedArgs model = new OnModeratorsReceivedArgs() - { - Channel = channel, - Moderators = moderators - }; - client.RaiseEvent("OnModeratorsReceived", model); - } - - /// - /// Invokes the new subscriber. - /// - /// The client. - /// The badges. - /// The color hexadecimal. - /// The color. - /// The display name. - /// The emote set. - /// The identifier. - /// The login. - /// The system message. - /// The system message parsed. - /// The resub message. - /// The subscription plan. - /// Name of the subscription plan. - /// The room identifier. - /// The user identifier. - /// if set to true [is moderator]. - /// if set to true [is turbo]. - /// if set to true [is subscriber]. - /// if set to true [is partner]. - /// The tmi sent ts. - /// Type of the user. - /// The raw irc. - /// The channel. - public static void InvokeNewSubscriber(this TwitchClient client, List> badges, List> badgeInfo, string colorHex, Color color, string displayName, - string emoteSet, string id, string login, string systemMessage, string msgId, string msgParamCumulativeMonths, string msgParamStreakMonths, bool msgParamShouldShareStreak, string systemMessageParsed, string resubMessage, SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, string roomId, string userId, bool isModerator, bool isTurbo, bool isSubscriber, bool isPartner, string tmiSentTs, - UserType userType, string rawIrc, string channel) - { - OnNewSubscriberArgs model = new OnNewSubscriberArgs() - { - Subscriber = new Subscriber(badges, badgeInfo, colorHex, color, displayName, emoteSet, id, login, systemMessage, msgId, msgParamCumulativeMonths, msgParamStreakMonths, msgParamShouldShareStreak, systemMessageParsed, resubMessage, - subscriptionPlan, subscriptionPlanName, roomId, userId, isModerator, isTurbo, isSubscriber, isPartner, tmiSentTs, userType, rawIrc, channel) - }; - client.RaiseEvent("OnNewSubscriber", model); - } - - /// - /// Invokes the raid notification. - /// - /// The client. - /// The channel. - /// The badges. - /// The color. - /// The display name. - /// The emotes. - /// The identifier. - /// The login. - /// if set to true [moderator]. - /// The MSG identifier. - /// Display name of the MSG parameter. - /// The MSG parameter login. - /// The MSG parameter viewer count. - /// The room identifier. - /// if set to true [subscriber]. - /// The system MSG. - /// The system MSG parsed. - /// The tmi sent ts. - /// if set to true [turbo]. - /// Type of the user. - /// Id of user. - public static void InvokeRaidNotification(this TwitchClient client, string channel, List> badges, List> badgeInfo, string color, string displayName, string emotes, string id, string login, bool moderator, string msgId, string msgParamDisplayName, - string msgParamLogin, string msgParamViewerCount, string roomId, bool subscriber, string systemMsg, string systemMsgParsed, string tmiSentTs, bool turbo, UserType userType, string userId) - { - OnRaidNotificationArgs model = new OnRaidNotificationArgs() - { - Channel = channel, - RaidNotification = new RaidNotification(badges, badgeInfo, color, displayName, emotes, id, login, moderator, msgId, msgParamDisplayName, msgParamLogin, msgParamViewerCount, - roomId, subscriber, systemMsg, systemMsgParsed, tmiSentTs, turbo, userType, userId) - }; - client.RaiseEvent("OnRaidNotification", model); - } - - /// - /// Invokes the re subscriber. - /// - /// The client. - /// The badges. - /// The color hexadecimal. - /// The color. - /// The display name. - /// The emote set. - /// The identifier. - /// The login. - /// The system message. - /// The system message parsed. - /// The resub message. - /// The subscription plan. - /// Name of the subscription plan. - /// The room identifier. - /// The user identifier. - /// if set to true [is moderator]. - /// if set to true [is turbo]. - /// if set to true [is subscriber]. - /// if set to true [is partner]. - /// The tmi sent ts. - /// Type of the user. - /// The raw irc. - /// The channel. - public static void InvokeReSubscriber(this TwitchClient client, List> badges, List> badgeInfo, string colorHex, Color color, string displayName, - string emoteSet, string id, string login, string systemMessage, string msgId, string msgParamCumulativeMonths, string msgParamStreakMonths, bool msgParamShouldShareStreak, string systemMessageParsed, string resubMessage, SubscriptionPlan subscriptionPlan, - string subscriptionPlanName, string roomId, string userId, bool isModerator, bool isTurbo, bool isSubscriber, bool isPartner, string tmiSentTs, - UserType userType, string rawIrc, string channel) - { - OnReSubscriberArgs model = new OnReSubscriberArgs() - { - ReSubscriber = new ReSubscriber(badges, badgeInfo, colorHex, color, displayName, emoteSet, id, login, systemMessage, msgId, msgParamCumulativeMonths, msgParamStreakMonths, msgParamShouldShareStreak, systemMessageParsed, resubMessage, - subscriptionPlan, subscriptionPlanName, roomId, userId, isModerator, isTurbo, isSubscriber, isPartner, tmiSentTs, userType, rawIrc, channel) - }; - client.RaiseEvent("OnReSubscriber", model); - } - - /// - /// Invokes the send receive data. - /// - /// The client. - /// The data. - /// The direction. - public static void InvokeSendReceiveData(this TwitchClient client, string data, SendReceiveDirection direction) - { - OnSendReceiveDataArgs model = new OnSendReceiveDataArgs() - { - Data = data, - Direction = direction - }; - client.RaiseEvent("OnSendReceiveData", model); - } - - /// - /// Invokes the user banned. - /// - /// The client. - /// The channel. - /// The username. - /// The ban reason. - /// The channel id. - /// The user id. - public static void InvokeUserBanned(this TwitchClient client, string channel, string username, string banReason, string roomId, string targetUserId) - { - OnUserBannedArgs model = new OnUserBannedArgs() - { - UserBan = new UserBan(channel, username, banReason, roomId, targetUserId) - }; - client.RaiseEvent("OnUserBanned", model); - } - - /// - /// Invokes the user joined. - /// - /// The client. - /// The channel. - /// The username. - public static void InvokeUserJoined(this TwitchClient client, string channel, string username) - { - OnUserJoinedArgs model = new OnUserJoinedArgs() - { - Channel = channel, - Username = username - }; - client.RaiseEvent("OnUserJoined", model); - } - - /// - /// Invokes the user left. - /// - /// The client. - /// The channel. - /// The username. - public static void InvokeUserLeft(this TwitchClient client, string channel, string username) - { - OnUserLeftArgs model = new OnUserLeftArgs() - { - Channel = channel, - Username = username - }; - client.RaiseEvent("OnUserLeft", model); - } - - /// - /// Invokes the user state changed. - /// - /// The client. - /// The badges. - /// The color hexadecimal. - /// The display name. - /// The emote set. - /// The channel. - /// The Id. - /// if set to true [is subscriber]. - /// if set to true [is moderator]. - /// Type of the user. - public static void InvokeUserStateChanged(this TwitchClient client, List> badges, List> badgeInfo, string colorHex, string displayName, - string emoteSet, string channel, string id, bool isSubscriber, bool isModerator, UserType userType) - { - OnUserStateChangedArgs model = new OnUserStateChangedArgs() - { - UserState = new UserState(badges, badgeInfo, colorHex, displayName, emoteSet, channel, id, isSubscriber, isModerator, userType) - }; - client.RaiseEvent("OnUserStateChanged", model); - } - - /// - /// Invokes the user timedout. - /// - /// The client. - /// The channel. - /// The username. - /// The user Id. - /// Duration of the timeout. - /// The timeout reason. - public static void InvokeUserTimedout(this TwitchClient client, string channel, string username, string targetUserId, int timeoutDuration, string timeoutReason) - { - OnUserTimedoutArgs model = new OnUserTimedoutArgs() - { - UserTimeout = new UserTimeout(channel, username, targetUserId, timeoutDuration, timeoutReason) - }; - client.RaiseEvent("OnUserTimedout", model); - } - - /// - /// Invokes the whisper command received. - /// - /// The client. - /// The badges. - /// The color hexadecimal. - /// The color. - /// The username. - /// The display name. - /// The emote set. - /// The thread identifier. - /// The message identifier. - /// The user identifier. - /// if set to true [is turbo]. - /// The bot username. - /// The message. - /// Type of the user. - /// The command text. - /// The arguments as string. - /// The arguments as list. - /// The command identifier. - public static void InvokeWhisperCommandReceived(this TwitchClient client, List> badges, string colorHex, Color color, string username, string displayName, EmoteSet emoteSet, string threadId, string messageId, - string userId, bool isTurbo, string botUsername, string message, UserType userType, string commandText, string argumentsAsString, List argumentsAsList, char commandIdentifier) - { - WhisperMessage whisperMsg = new WhisperMessage(badges, colorHex, color, username, displayName, emoteSet, threadId, messageId, userId, isTurbo, botUsername, message, userType); - OnWhisperCommandReceivedArgs model = new OnWhisperCommandReceivedArgs() - { - Command = new WhisperCommand(whisperMsg, commandText, argumentsAsString, argumentsAsList, commandIdentifier) - }; - client.RaiseEvent("OnWhisperCommandReceived", model); - } - - /// - /// Invokes the whisper received. - /// - /// The client. - /// The badges. - /// The color hexadecimal. - /// The color. - /// The username. - /// The display name. - /// The emote set. - /// The thread identifier. - /// The message identifier. - /// The user identifier. - /// if set to true [is turbo]. - /// The bot username. - /// The message. - /// Type of the user. - public static void InvokeWhisperReceived(this TwitchClient client, List> badges, string colorHex, Color color, string username, string displayName, EmoteSet emoteSet, string threadId, string messageId, - string userId, bool isTurbo, string botUsername, string message, UserType userType) - { - OnWhisperReceivedArgs model = new OnWhisperReceivedArgs() - { - WhisperMessage = new WhisperMessage(badges, colorHex, color, username, displayName, emoteSet, threadId, messageId, userId, isTurbo, botUsername, message, userType) - }; - client.RaiseEvent("OnWhisperReceived", model); - } - - /// - /// Invokes the whisper sent. - /// - /// The client. - /// The username. - /// The receiver. - /// The message. - public static void InvokeWhisperSent(this TwitchClient client, string username, string receiver, string message) - { - OnWhisperSentArgs model = new OnWhisperSentArgs() - { - Message = message, - Receiver = receiver, - Username = username - }; - client.RaiseEvent("OnWhisperSent", model); - } - } -} diff --git a/TwitchLib.Client/Extensions/EventInvocationExtensions.cs b/TwitchLib.Client/Extensions/EventInvocationExtensions.cs new file mode 100644 index 00000000..d5234156 --- /dev/null +++ b/TwitchLib.Client/Extensions/EventInvocationExtensions.cs @@ -0,0 +1,17 @@ +using TwitchLib.Communication.Events; + +namespace TwitchLib.Client.Extensions; + +/// +/// Extends logic for handling events. +/// +internal static class EventInvocationExtensions +{ + /// + /// Invokes the event handler when it is not null. Returns a completed task otherwise. + /// + public static Task TryInvoke(this AsyncEventHandler? eventHandler, object? sender, TEventArgs eventArgs) + { + return eventHandler?.Invoke(sender, eventArgs) ?? Task.CompletedTask; + } +} diff --git a/TwitchLib.Client/Extensions/FollowersOnlyExt.cs b/TwitchLib.Client/Extensions/FollowersOnlyExt.cs deleted file mode 100644 index 804b8673..00000000 --- a/TwitchLib.Client/Extensions/FollowersOnlyExt.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using TwitchLib.Client.Exceptions; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing followers online mode functionality in TwitchClient - /// - public static class FollowersOnlyExt - { - // Using variable so it's easily changed if Twitch changes their requirement. - /// - /// The maximum duration allowed days - /// - private const int MaximumDurationAllowedDays = 90; - - /// - /// Enables follower only chat, requires a TimeSpan object to indicate how long a viewer must have been following to chat. Maximum time is 90 days (3 months). - /// - /// Client reference used to identify extension. - /// JoinedChannel object representing which channel to send command to. - /// Amount of time required to pass before a viewer can chat. Maximum is 3 months (90 days). - /// The amount of time required to chat exceeded the maximum allowed by Twitch, which is 3 months. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void FollowersOnlyOn(this ITwitchClient client, JoinedChannel channel, TimeSpan requiredFollowTime) - { - if (requiredFollowTime > TimeSpan.FromDays(MaximumDurationAllowedDays)) - throw new InvalidParameterException("The amount of time required to chat exceeded the maximum allowed by Twitch, which is 3 months.", client.TwitchUsername); - - string duration = $"{requiredFollowTime.Days}d {requiredFollowTime.Hours}h {requiredFollowTime.Minutes}m"; - - client.SendMessage(channel, $".followers {duration}"); - } - - /// - /// Enables follower only chat, requires a TimeSpan object to indicate how long a viewer must have been following to chat. Maximum time is 90 days (3 months). - /// - /// Client reference used to identify extension. - /// String representing the channel to send the command to. - /// Amount of time required to pass before a viewer can chat. Maximum is 3 months (90 days). - /// The amount of time required to chat exceeded the maximum allowed by Twitch, which is 3 months. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void FollowersOnlyOn(this ITwitchClient client, string channel, TimeSpan requiredFollowTime) - { - if (requiredFollowTime > TimeSpan.FromDays(MaximumDurationAllowedDays)) - throw new InvalidParameterException("The amount of time required to chat exceeded the maximum allowed by Twitch, which is 3 months.", client.TwitchUsername); - - string duration = $"{requiredFollowTime.Days}d {requiredFollowTime.Hours}h {requiredFollowTime.Minutes}m"; - - client.SendMessage(channel, $".followers {duration}"); - } - - /// - /// Disables follower only chat. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of channel to send command to - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void FollowersOnlyOff(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".followersoff"); - } - - /// - /// Disables follower only chat. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send the command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void FollowersOnlyOff(this TwitchClient client, string channel) - { - client.SendMessage(channel, ".followersoff"); - } - } -} diff --git a/TwitchLib.Client/Extensions/GetChannelModeratorsExt.cs b/TwitchLib.Client/Extensions/GetChannelModeratorsExt.cs deleted file mode 100644 index f4601561..00000000 --- a/TwitchLib.Client/Extensions/GetChannelModeratorsExt.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing Commercial functionality in TwitchClient. - /// - public static class GetChannelModeratorsExt - { - /// - /// Sends command to get list of moderators from Twitch. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the command to get moderators to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.GetModeratorsAsync() instead.")] - public static void GetChannelModerators(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".mods"); - } - - /// - /// Sends command to get list of moderators from Twitch. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the command to get moderators to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.GetModeratorsAsync() instead.")] - public static void GetChannelModerators(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".mods"); - } - } -} diff --git a/TwitchLib.Client/Extensions/LogExtensions.cs b/TwitchLib.Client/Extensions/LogExtensions.cs new file mode 100644 index 00000000..7aff1588 --- /dev/null +++ b/TwitchLib.Client/Extensions/LogExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; + +namespace TwitchLib.Client.Extensions; + +internal static partial class LogExtensions +{ + [LoggerMessage(LogLevel.Information, "Connecting Twitch Chat Client...")] + public static partial void LogConnecting(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Disconnecting Twitch Chat Client...")] + public static partial void LogDisconnecting(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "TwitchLib-TwitchClient initialized, assembly version: {version}")] + public static partial void LogInitialized(this ILogger logger, Version version); + + [LoggerMessage(LogLevel.Information, "Joining channel: {channel}")] + public static partial void LogJoiningChannel(this ILogger logger, string channel); + + [LoggerMessage(LogLevel.Debug, "Finished channel joining queue.")] + public static partial void LogChannelJoiningFinished(this ILogger logger); + + [LoggerMessage(LogLevel.Information, "Leaving channel: {channel}")] + public static partial void LogLeavingChannel(this ILogger logger, string channel); + + [LoggerMessage(LogLevel.Error, "Message length has exceeded the maximum character count. (500)")] + public static partial void LogMessageTooLong(this ILogger logger); + + [LoggerMessage(LogLevel.Trace, "Received: {line}")] + public static partial void LogReceived(this ILogger logger, string line); + + [LoggerMessage(LogLevel.Information, "Reconnecting to Twitch")] + public static partial void LogReconnecting(this ILogger logger); + + [LoggerMessage(LogLevel.Debug, "Should be connected!")] + public static partial void LogShouldBeConnected(this ILogger logger); + + [LoggerMessage(LogLevel.Warning, "Unaccounted for: {ircString} (please create a TwitchLib GitHub issue :P)")] + public static partial void LogUnaccountedFor(this ILogger logger, string ircString); + + [LoggerMessage(LogLevel.Error, "Unexpected error during message parsing, message: {message}")] + public static partial void LogParsingError(this ILogger logger, string message, Exception ex); + + [LoggerMessage(LogLevel.Debug, "Writing: {message}")] + public static partial void LogWriting(this ILogger logger, string message); + + [LoggerMessage(LogLevel.Error, "{message}")] + public static partial void LogException(this ILogger logger, string message, Exception ex); +} diff --git a/TwitchLib.Client/Extensions/MarkerExt.cs b/TwitchLib.Client/Extensions/MarkerExt.cs deleted file mode 100644 index 96127745..00000000 --- a/TwitchLib.Client/Extensions/MarkerExt.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for implementing marker functionality - /// - public static class MarkerExt - { - /// - /// Sends command to create a marker using a JoinedChannel object. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the marker command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Streams.CreateStreamMarkerAsync() instead.")] - public static void Marker(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".marker"); - } - - /// - /// Sends command to create a marker using a string. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the marker command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Streams.CreateStreamMarkerAsync() instead.")] - public static void Marker(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".marker"); - } - } -} diff --git a/TwitchLib.Client/Extensions/ModExt.cs b/TwitchLib.Client/Extensions/ModExt.cs deleted file mode 100644 index c3753ffd..00000000 --- a/TwitchLib.Client/Extensions/ModExt.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing the ability to mod a viewer in TwitchClient. - /// - public static class ModExt - { - /// - /// Sends a command to make a specific viewer a moderator. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send the command to. - /// Username of the viewer to make a moderator. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.AddChannelModeratorAsync() instead.")] - public static void Mod(this ITwitchClient client, JoinedChannel channel, string viewerToMod) - { - client.SendMessage(channel, $".mod {viewerToMod}"); - } - - /// - /// Sends a command to make a specific viewer a moderator. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send the command to. - /// Username of the viewer to make a moderator. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.AddChannelModeratorAsync() instead.")] - public static void Mod(this ITwitchClient client, string channel, string viewerToMod) - { - client.SendMessage(channel, $".mod {viewerToMod}"); - } - - /// - /// Sends a command to remove moderator status from a specific viewer - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send the command to. - /// Username of the viewer to remove moderator status from. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChannelModeratorAsync() instead.")] - public static void Unmod(this ITwitchClient client, JoinedChannel channel, string viewerToUnmod) - { - client.SendMessage(channel, $".unmod {viewerToUnmod}"); - } - - /// - /// Sends a command to remove moderator status from a specific viewer - /// - /// Client reference used to identify extension. - /// String representation of which channel to send the command to. - /// Username of the viewer to remove moderator status from. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.DeleteChannelModeratorAsync() instead.")] - public static void Unmod(this ITwitchClient client, string channel, string viewerToUnmod) - { - client.SendMessage(channel, $".unmod {viewerToUnmod}"); - } - } -} diff --git a/TwitchLib.Client/Extensions/RaidExt.cs b/TwitchLib.Client/Extensions/RaidExt.cs deleted file mode 100644 index ab0d446c..00000000 --- a/TwitchLib.Client/Extensions/RaidExt.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing the ability to start raids via TwitchClient. - /// - public static class RaidExt - { - /// - /// Sends command to start raid. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send the command to. - /// Channel to begin raid on. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Raids.StartRaidAsync() instead.")] - public static void Raid(this ITwitchClient client, JoinedChannel channel, string channelToRaid) - { - client.SendMessage(channel, $".raid {channelToRaid}"); - } - - /// - /// Sends command to start raid. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send the command to. - /// Channel to begin raid on. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Raids.StartRaidAsync() instead.")] - public static void Raid(this ITwitchClient client, string channel, string channelToRaid) - { - client.SendMessage(channel, $".raid {channelToRaid}"); - } - } -} diff --git a/TwitchLib.Client/Extensions/ReplyWhisperExt.cs b/TwitchLib.Client/Extensions/ReplyWhisperExt.cs deleted file mode 100644 index 39488b8c..00000000 --- a/TwitchLib.Client/Extensions/ReplyWhisperExt.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing reply to previous whisper functionality. - /// - public static class ReplyWhisperExt - { - /// - /// SendWhisper wrapper method that will send a whisper back to the user who most recently sent a whisper to this bot. - /// - /// The client. - /// The message. - /// if set to true [dry run]. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Whispers.SendWhisperAsync() instead.")] - public static void ReplyToLastWhisper(this ITwitchClient client, string message = "", bool dryRun = false) - { - if (client.PreviousWhisper != null) - client.SendWhisper(client.PreviousWhisper.Username, message, dryRun); - } - } -} diff --git a/TwitchLib.Client/Extensions/SlowModeExt.cs b/TwitchLib.Client/Extensions/SlowModeExt.cs deleted file mode 100644 index 786631f0..00000000 --- a/TwitchLib.Client/Extensions/SlowModeExt.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using TwitchLib.Client.Exceptions; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension to implement slowmode functionality in TwitchClient - /// - public static class SlowModeExt - { - /// - /// Enables slow mode. messageCooldown must be less than 1 day. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send the slow command to. - /// TimeSpan object representing how long message cooldowns should be. May not exceed 1 day total. - /// The message cooldown time supplied exceeded the maximum allowed by Twitch, which is 1 day. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SlowModeOn(this ITwitchClient client, JoinedChannel channel, TimeSpan messageCooldown) - { - if (messageCooldown > TimeSpan.FromDays(1)) - throw new InvalidParameterException("The message cooldown time supplied exceeded the maximum allowed by Twitch, which is 1 day.", client.TwitchUsername); - - client.SendMessage(channel, $".slow {messageCooldown.TotalSeconds}"); - } - - /// - /// Enables slow mode. messageCooldown must be less than 1 day. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send the slow command to. - /// TimeSpan object representing how long message cooldowns should be. May not exceed 1 day total. - /// The message cooldown time supplied exceeded the maximum allowed by Twitch, which is 1 day. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SlowModeOn(this ITwitchClient client, string channel, TimeSpan messageCooldown) - { - if (messageCooldown > TimeSpan.FromDays(1)) - throw new InvalidParameterException("The message cooldown time supplied exceeded the maximum allowed by Twitch, which is 1 day.", client.TwitchUsername); - - client.SendMessage(channel, $".slow {messageCooldown.TotalSeconds}"); - } - - /// - /// Disables slow mode. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send slowoff command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SlowModeOff(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".slowoff"); - } - - /// - /// Disables slow mode. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send slowoff command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SlowModeOff(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".slowoff"); - } - } -} diff --git a/TwitchLib.Client/Extensions/SplitExtensions.cs b/TwitchLib.Client/Extensions/SplitExtensions.cs new file mode 100644 index 00000000..ae04b8e1 --- /dev/null +++ b/TwitchLib.Client/Extensions/SplitExtensions.cs @@ -0,0 +1,100 @@ +// proposal: https://github.com/dotnet/runtime/issues/75317 +using System.Runtime.CompilerServices; + +namespace TwitchLib.Client.Extensions +{ + internal static class SplitExtensions + { + /// + /// Splits the string 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. + /// + /// Source span to split + /// Separator value + /// A split pair of Segment and Remainder, deconstructible with tuple pattern. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySplitPair SplitFirst(this string source, char separator) + { + return SplitFirst(source.AsSpan(), separator); + } + + /// + /// 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. + /// + /// Span element type + /// Source span to split + /// Separator value + /// A split pair of Segment and Remainder, deconstructible with tuple pattern. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySplitPair SplitFirst(this ReadOnlySpan source, T separator) + where T : IEquatable + { + var separatorIndex = source.IndexOf(separator); + + return separatorIndex >= 0 ? new(source, separatorIndex, 1) : SplitNotFound(source); + } + + /// + /// Splits the string into two parts at the last occurrence of a separator. + /// If the separator is not found, Segment will be the entire span and Remainder will be empty. + /// + /// Source span to split + /// Separator value + /// A split pair of Segment and Remainder, deconstructible with tuple pattern. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySplitPair SplitLast(this string source, char separator) + { + return SplitLast(source.AsSpan(), separator); + } + + /// + /// Splits the span into two parts at the last occurrence of a separator. + /// If the separator is not found, Segment will be the entire span and Remainder will be empty. + /// + /// Span element type + /// Source span to split + /// Separator value + /// A split pair of Segment and Remainder, deconstructible with tuple pattern. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySplitPair SplitLast(this ReadOnlySpan source, T separator) + where T : IEquatable + { + var separatorIndex = source.LastIndexOf(separator); + + return separatorIndex >= 0 ? new(source, separatorIndex, 1) : SplitNotFound(source); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ReadOnlySplitPair SplitNotFound(this ReadOnlySpan source) + { + return new(source, source.Length, 0); + } + + internal readonly ref struct ReadOnlySplitPair + { + private readonly ReadOnlySpan _source; + private readonly int _offset; + private readonly int _stride; + + public readonly ReadOnlySpan Segment => _source.Slice(0, _offset); + + public readonly ReadOnlySpan Remainder => _source.Slice(_offset + _stride); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySplitPair(ReadOnlySpan source, int offset, int stride) + { + _source = source; + _offset = offset; + _stride = stride; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Deconstruct(out ReadOnlySpan segment, out ReadOnlySpan remainder) + { + segment = Segment; + remainder = Remainder; + } + } + } +} diff --git a/TwitchLib.Client/Extensions/SubscribersOnly.cs b/TwitchLib.Client/Extensions/SubscribersOnly.cs deleted file mode 100644 index a202f796..00000000 --- a/TwitchLib.Client/Extensions/SubscribersOnly.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing subscriber only functionality in TwitchClient - /// - public static class SubscribersOnly - { - /// - /// Enables subscriber only mode in chat. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send subscriber only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SubscribersOnlyOn(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".subscribers"); - } - - /// - /// Enables subscriber only mode in chat. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send subscriber only command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SubscribersOnlyOn(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".subscribers"); - } - - /// - /// Enables subscriber only mode in chat. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of which channel to send subscriber only off command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SubscribersOnlyOff(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".subscribersoff"); - } - - /// - /// Disables subscriber only mode in chat. - /// - /// Client reference used to identify extension. - /// String representation of which channel to send subscriber only off command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Chat.UpdateChatSettingsAsync() instead.")] - public static void SubscribersOnlyOff(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".subscribersoff"); - } - } -} diff --git a/TwitchLib.Client/Extensions/TimeoutUserExt.cs b/TwitchLib.Client/Extensions/TimeoutUserExt.cs deleted file mode 100644 index c2ac2592..00000000 --- a/TwitchLib.Client/Extensions/TimeoutUserExt.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension implementing timeout functionality in TwitchClient - /// - public static class TimeoutUserExt - { - #region TimeoutUser - /// - /// TImesout a user in chat using a JoinedChannel object. - /// - /// Client reference used to identify extension. - /// Channel object to send timeout to - /// Viewer name to timeout - /// Duration of the timeout via TimeSpan object - /// Message to accompany the timeout and show the user. - /// Indicates a dryrun (will not sened if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.BanUserAsync() instead.")] - public static void TimeoutUser(this ITwitchClient client, JoinedChannel channel, string viewer, TimeSpan duration, string message = "", bool dryRun = false) - { - client.SendMessage(channel, $".timeout {viewer} {duration.TotalSeconds} {message}", dryRun); - } - - /// - /// Timesout a user in chat using a string for the channel. - /// - /// Client reference used to identify extension. - /// Channel in string form to send timeout to - /// Viewer name to timeout - /// Duration of the timeout via TimeSpan object - /// Message to accompany the timeout and show the user. - /// Indicates a dryrun (will not sened if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.BanUserAsync() instead.")] - public static void TimeoutUser(this ITwitchClient client, string channel, string viewer, TimeSpan duration, string message = "", bool dryRun = false) - { - JoinedChannel joinedChannel = client.GetJoinedChannel(channel); - if (joinedChannel != null) - TimeoutUser(client, joinedChannel, viewer, duration, message, dryRun); - } - #endregion - } -} diff --git a/TwitchLib.Client/Extensions/UnbanUserExt.cs b/TwitchLib.Client/Extensions/UnbanUserExt.cs deleted file mode 100644 index 093f5cda..00000000 --- a/TwitchLib.Client/Extensions/UnbanUserExt.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension to implement unban functionality. - /// - public static class UnbanUserExt - { - /// - /// Unbans a user in chat using JoinedChannel - /// - /// Client reference used to identify extension. - /// JoinedChannel object to send unban to - /// Viewer name to unban - /// Indicates a dryrun (will not send if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.UnbanUserAsync() instead.")] - public static void UnbanUser(this ITwitchClient client, JoinedChannel channel, string viewer, bool dryRun = false) - { - client.SendMessage(channel, $".unban {viewer}", dryRun); - } - - /// - /// Unbans a user in chat using a string for the channel - /// - /// Client reference used to identify extension. - /// Channel in string form to send unban to - /// Viewer name to unban - /// Indicates a dryrun (will not send if true) - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Moderation.UnbanUserAsync() instead.")] - public static void UnbanUser(this ITwitchClient client, string channel, string viewer, bool dryRun = false) - { - JoinedChannel joinedChannel = client.GetJoinedChannel(channel); - if (joinedChannel != null) - UnbanUser(client, joinedChannel, viewer, dryRun); - } - } -} diff --git a/TwitchLib.Client/Extensions/VipExt.cs b/TwitchLib.Client/Extensions/VipExt.cs deleted file mode 100644 index f64fe843..00000000 --- a/TwitchLib.Client/Extensions/VipExt.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using TwitchLib.Client.Interfaces; -using TwitchLib.Client.Models; - -namespace TwitchLib.Client.Extensions -{ - /// - /// Extension for using VIP related commands in TwitchClient - /// - public static class VIPExt - { - /// - /// Creates new VIP user (REQUIRES SCOPE: channel:moderate) - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the VIP command to. - /// The viewer to vip. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.AddChannelVIPAsync() instead.")] - public static void VIP(this ITwitchClient client, JoinedChannel channel, string viewerToVIP) - { - client.SendMessage(channel, $".vip {viewerToVIP}"); - } - - /// - /// Creates new VIP user (REQUIRES SCOPE: channel:moderate) - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the VIP command to. - /// The viewer to vip. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.AddChannelVIPAsync() instead.")] - public static void VIP(this ITwitchClient client, string channel, string viewerToVIP) - { - client.SendMessage(channel, $".vip {viewerToVIP}"); - } - - /// - /// Removes VIP status from user (REQUIRES SCOPE: channel:moderate) - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the unvip command to. - /// The viewer to un vip. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.RemoveChannelVIPAsync() instead.")] - public static void UnVIP(this ITwitchClient client, JoinedChannel channel, string viewerToUnVIP) - { - client.SendMessage(channel, $".unvip {viewerToUnVIP}"); - } - - /// - /// Removes VIP status from user (REQUIRES SCOPE: channel:moderate) - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the unvip command to. - /// The viewer to un vip. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.RemoveChannelVIPAsync() instead.")] - public static void UnVIP(this ITwitchClient client, string channel, string viewerToUnVIP) - { - client.SendMessage(channel, $".unvip {viewerToUnVIP}"); - } - - /// - /// Asks Twitch for a list of VIPs in the channel. Listen to OnVIPsReceived event for the response. - /// - /// Client reference used to identify extension. - /// JoinedChannel representation of the channel to send the vips command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.GetVIPsAsync() instead.")] - public static void GetVIPs(this ITwitchClient client, JoinedChannel channel) - { - client.SendMessage(channel, ".vips"); - } - - /// - /// Asks Twitch for a list of VIPs in the channel. Listen to OnVIPsReceived event for the response. - /// - /// Client reference used to identify extension. - /// String representation of the channel to send the vips command to. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Channels.GetVIPsAsync() instead.")] - public static void GetVIPs(this ITwitchClient client, string channel) - { - client.SendMessage(channel, ".vips"); - } - } -} diff --git a/TwitchLib.Client/Interfaces/ITwitchClient.cs b/TwitchLib.Client/Interfaces/ITwitchClient.cs index 5d8b280c..b9ee3192 100644 --- a/TwitchLib.Client/Interfaces/ITwitchClient.cs +++ b/TwitchLib.Client/Interfaces/ITwitchClient.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using TwitchLib.Client.Events; +using TwitchLib.Client.Events; using TwitchLib.Client.Models; using TwitchLib.Communication.Events; @@ -11,11 +9,6 @@ namespace TwitchLib.Client.Interfaces /// public interface ITwitchClient { - /// - /// Gets or sets a value indicating whether [automatic re listen on exception]. - /// - /// true if [automatic re listen on exception]; otherwise, false. - bool AutoReListenOnException { get; set; } /// /// Gets the channel emotes. /// @@ -25,7 +18,7 @@ public interface ITwitchClient /// Gets the connection credentials. /// /// The connection credentials. - ConnectionCredentials ConnectionCredentials { get; } + ConnectionCredentials? ConnectionCredentials { get; } /// /// Gets or sets a value indicating whether [disable automatic pong]. /// @@ -50,7 +43,7 @@ public interface ITwitchClient /// Gets the previous whisper. /// /// The previous whisper. - WhisperMessage PreviousWhisper { get; } + WhisperMessage? PreviousWhisper { get; } /// /// Gets the twitch username. /// @@ -62,198 +55,284 @@ public interface ITwitchClient /// true if [will replace emotes]; otherwise, false. bool WillReplaceEmotes { get; set; } /// - /// Occurs when [on channel state changed]. + /// The chat command identifiers /// - event EventHandler OnChannelStateChanged; + ICollection ChatCommandIdentifiers { get; } /// - /// Occurs when [on chat cleared]. + /// The whisper command identifiers /// - event EventHandler OnChatCleared; + ICollection WhisperCommandIdentifiers { get; } + /// - /// Occurs when [on chat color changed]. + /// Fires when an Announcement is received /// - event EventHandler OnChatColorChanged; + event AsyncEventHandler? OnAnnouncement; + /// - /// Occurs when [on chat command received]. + /// Fires when client connects to Twitch. /// - event EventHandler OnChatCommandReceived; + event AsyncEventHandler? OnConnected; + /// - /// Occurs when [on connected]. + /// Fires when client joins a channel. /// - event EventHandler OnConnected; + event AsyncEventHandler? OnJoinedChannel; + /// - /// Occurs when [on connection error]. + /// Fires on logging in with incorrect details, returns ErrorLoggingInException. /// - event EventHandler OnConnectionError; + event AsyncEventHandler? OnIncorrectLogin; + /// - /// Occurs when [on disconnected]. + /// Fires when connecting and channel state is changed, returns ChannelState. /// - event EventHandler OnDisconnected; + event AsyncEventHandler? OnChannelStateChanged; + /// - /// Occurs when [on existing users detected]. + /// Fires when a user state is received, returns UserState. /// - event EventHandler OnExistingUsersDetected; + event AsyncEventHandler? OnUserStateChanged; + /// - /// Occurs when [on gifted subscription]. + /// Fires when a new chat message arrives, returns ChatMessage. /// - event EventHandler OnGiftedSubscription; + event AsyncEventHandler? OnMessageReceived; + /// - /// Occurs when [on incorrect login]. + /// Fires when a new whisper arrives, returns WhisperMessage. /// - event EventHandler OnIncorrectLogin; + event AsyncEventHandler? OnWhisperReceived; + /// - /// Occurs when [on joined channel]. + /// Fires when a chat message is sent, returns username, channel and message. /// - event EventHandler OnJoinedChannel; + event AsyncEventHandler? OnMessageSent; + /// - /// Occurs when [on left channel]. + /// Fires when command (uses custom chat command identifier) is received, returns channel, command, ChatMessage, arguments as string, arguments as list. /// - event EventHandler OnLeftChannel; + event AsyncEventHandler? OnChatCommandReceived; + /// - /// Occurs when [on log]. + /// Fires when command (uses custom whisper command identifier) is received, returns command, Whispermessage. /// - event EventHandler OnLog; + event AsyncEventHandler? OnWhisperCommandReceived; + /// - /// Occurs when [on message received]. + /// Fires when a new viewer/chatter joined the channel's chat room, returns username and channel. /// - event EventHandler OnMessageReceived; + event AsyncEventHandler? OnUserJoined; + /// - /// Occurs when [on message sent]. + /// Fires when a message gets deleted in chat. /// - event EventHandler OnMessageSent; + event AsyncEventHandler? OnMessageCleared; + /// - /// Occurs when [on moderator joined]. + /// Fires when new subscriber is announced in chat, returns Subscriber. /// - event EventHandler OnModeratorJoined; + event AsyncEventHandler? OnNewSubscriber; + /// - /// Occurs when [on moderator left]. + /// Fires when current subscriber renews subscription, returns ReSubscriber. /// - event EventHandler OnModeratorLeft; + event AsyncEventHandler? OnReSubscriber; + /// - /// Occurs when [on moderators received]. + /// Fires when a current Prime gaming subscriber converts to a paid subscription. /// - event EventHandler OnModeratorsReceived; + event AsyncEventHandler? OnPrimePaidSubscriber; + /// - /// Occurs when [on new subscriber]. + /// Fires when Twitch notifies client of existing users in chat. /// - event EventHandler OnNewSubscriber; + event AsyncEventHandler? OnExistingUsersDetected; + /// - /// Occurs when [on raid notification]. + /// Fires when a PART message is received from Twitch regarding a particular viewer /// - event EventHandler OnRaidNotification; + event AsyncEventHandler? OnUserLeft; + /// - /// Occurs when [on re subscriber]. + /// Fires when bot has disconnected. /// - event EventHandler OnReSubscriber; + event AsyncEventHandler? OnDisconnected; + /// - /// Occurs when [on send receive data]. + /// Forces when bot suffers connection error. /// - event EventHandler OnSendReceiveData; + event AsyncEventHandler? OnConnectionError; + /// - /// Occurs when [on user banned]. + /// Fires when a channel's chat is cleared. /// - event EventHandler OnUserBanned; + event AsyncEventHandler? OnChatCleared; + /// - /// Occurs when [on user joined]. + /// Fires when a viewer gets timedout by any moderator. /// - event EventHandler OnUserJoined; + event AsyncEventHandler? OnUserTimedout; + /// - /// Occurs when [on user left]. + /// Fires when client successfully leaves a channel. /// - event EventHandler OnUserLeft; + event AsyncEventHandler? OnLeftChannel; + /// - /// Occurs when [on user state changed]. + /// Fires when a viewer gets banned by any moderator. /// - event EventHandler OnUserStateChanged; + event AsyncEventHandler? OnUserBanned; + /// - /// Occurs when [on user timedout]. + /// Fires when data is either received or sent. /// - event EventHandler OnUserTimedout; + event AsyncEventHandler? OnSendReceiveData; + /// - /// Occurs when [on whisper command received]. + /// Fires when a raid notification is detected in chat /// - event EventHandler OnWhisperCommandReceived; + event AsyncEventHandler? OnRaidNotification; + /// - /// Occurs when [on whisper received]. + /// Fires when a subscription is gifted and announced in chat /// - event EventHandler OnWhisperReceived; + event AsyncEventHandler? OnGiftedSubscription; + /// - /// Occurs when [on whisper sent]. + /// Fires when a community subscription is announced in chat /// - event EventHandler OnWhisperSent; + event AsyncEventHandler? OnCommunitySubscription; + /// - /// Occurs when [on message throttled]. + /// Fires when a gifted subscription is continued and announced in chat /// - event EventHandler OnMessageThrottled; + event AsyncEventHandler? OnContinuedGiftedSubscription; + + public event AsyncEventHandler? OnAnonGiftPaidUpgrade; + public event AsyncEventHandler? OnUnraidNotification; + public event AsyncEventHandler? OnRitual; + public event AsyncEventHandler? OnBitsBadgeTier; + public event AsyncEventHandler? OnCommunityPayForward; + public event AsyncEventHandler? OnStandardPayForward; + /// - /// Occurs when [on whisper throttled]. + /// Fires when a Message has been throttled. /// - event EventHandler OnWhisperThrottled; + event AsyncEventHandler? OnMessageThrottled; + /// - /// Occurs when [on error]. + /// Occurs when an Error is thrown in the protocol client /// - event EventHandler OnError; + event AsyncEventHandler? OnError; + /// - /// Occurs when [on reconnected]. + /// Occurs when a reconnection occurs. /// - event EventHandler OnReconnected; + event AsyncEventHandler? OnReconnected; + /// - /// Occurs when [on vip received]. + /// Occurs when chatting in a channel that requires a verified email without a verified email attached to the account. /// - event EventHandler OnVIPsReceived; + event AsyncEventHandler? OnRequiresVerifiedEmail; + /// - /// Occurs when [on community subscription announcement received]. + /// Occurs when chatting in a channel that requires a verified phone number without a verified phone number attached to the account. /// - event EventHandler OnCommunitySubscription; + event AsyncEventHandler? OnRequiresVerifiedPhoneNumber; + /// - /// Occurs when [on message deleted]. + /// Occurs when send message rate limit has been applied to the client in a specific channel by Twitch /// - event EventHandler OnMessageCleared; + event AsyncEventHandler? OnRateLimit; /// - /// Occurs when chatting in a channel that requires a verified email without a verified email attached to the account. + /// Occurs when sending duplicate messages and user is not permitted to do so /// - event EventHandler OnRequiresVerifiedEmail; + event AsyncEventHandler? OnDuplicate; /// - /// Occurs when chatting in a channel that requires a verified phone number without a verified phone number attached to the account. + /// Occurs when chatting in a channel that the user is banned in bcs of an already banned alias with the same Email /// - event EventHandler OnRequiresVerifiedPhoneNumber; + event AsyncEventHandler? OnBannedEmailAlias; /// - /// Occurs when chatting in a channel that the user is banned in bcs of an already banned alias with the same Email + /// Fires when TwitchClient attempts to host a channel it is in. /// - event EventHandler OnBannedEmailAlias; + event AsyncEventHandler? OnSelfRaidError; + + /// + /// Fires when TwitchClient receives generic no permission error from Twitch. + /// + event AsyncEventHandler? OnNoPermissionError; + + /// + /// Fires when newly raided channel is mature audience only. + /// + event AsyncEventHandler? OnRaidedChannelIsMatureAudience; + + /// + /// Fires when the client was unable to join a channel. + /// + event AsyncEventHandler? OnFailureToReceiveJoinConfirmation; + + /// + /// Fires when the client attempts to send a message to a channel in followers only mode, as a non-follower + /// + event AsyncEventHandler? OnFollowersOnly; + + /// + /// Fires when the client attempts to send a message to a channel in subs only mode, as a non-sub + /// + event AsyncEventHandler? OnSubsOnly; + + /// + /// Fires when the client attempts to send a non-emote message to a channel in emotes only mode + /// + event AsyncEventHandler? OnEmoteOnly; + + /// + /// Fires when the client attempts to send a message to a channel that has been suspended + /// + event AsyncEventHandler? OnSuspended; + + /// + /// Fires when the client attempts to send a message to a channel they're banned in + /// + event AsyncEventHandler? OnBanned; + + /// + /// Fires when the client attempts to send a message in a channel with slow mode enabled, without cooldown expiring + /// + event AsyncEventHandler? OnSlowMode; + + /// + /// Fires when the client attempts to send a message in a channel with r9k mode enabled, and message was not permitted + /// + event AsyncEventHandler? OnR9kMode; /// /// Fires when the client receives a PRIVMSG tagged as an user-intro /// - event EventHandler OnUserIntro; + event AsyncEventHandler? OnUserIntro; /// - /// Fires when the client receives a USERNOTICE tagged as an announcement + /// Fires when data is received from Twitch that is not able to be parsed. /// - event EventHandler OnAnnouncement; + event AsyncEventHandler? OnUnaccountedFor; /// /// Initializes the specified credentials. /// /// The credentials. /// The channel. - /// The chat command identifier. - /// The whisper command identifier. - /// if set to true [automatic re listen on exceptions]. - void Initialize(ConnectionCredentials credentials, string channel = null, char chatCommandIdentifier = '!', char whisperCommandIdentifier = '!', bool autoReListenOnExceptions = true); + void Initialize(ConnectionCredentials credentials, string? channel = null); /// /// Initializes the specified credentials. /// /// The credentials. /// The channels to join once connected. - /// The chat command identifier. - /// The whisper command identifier. - /// if set to true [automatic re listen on exceptions]. - void Initialize(ConnectionCredentials credentials, List channels, char chatCommandIdentifier = '!', char whisperCommandIdentifier = '!', bool autoReListenOnExceptions = true); + void Initialize(ConnectionCredentials credentials, List channels); /// /// Sets the connection credentials. @@ -261,70 +340,53 @@ public interface ITwitchClient /// The credentials. void SetConnectionCredentials(ConnectionCredentials credentials); - /// - /// Adds the chat command identifier. - /// - /// The identifier. - void AddChatCommandIdentifier(char identifier); - /// - /// Adds the whisper command identifier. - /// - /// The identifier. - void AddWhisperCommandIdentifier(char identifier); - /// - /// Removes the chat command identifier. - /// - /// The identifier. - void RemoveChatCommandIdentifier(char identifier); - /// - /// Removes the whisper command identifier. - /// - /// The identifier. - void RemoveWhisperCommandIdentifier(char identifier); - /// /// Connects this instance. /// /// bool representing Connect() result - bool Connect(); + Task ConnectAsync(); + /// /// Disconnects this instance. /// - void Disconnect(); + Task DisconnectAsync(); + /// /// Reconnects this instance. /// - void Reconnect(); + Task ReconnectAsync(); /// /// Gets the joined channel. /// /// The channel. /// JoinedChannel. - JoinedChannel GetJoinedChannel(string channel); - + JoinedChannel? GetJoinedChannel(string channel); + /// /// Joins the channel. /// /// The channel. /// if set to true [override check]. - void JoinChannel(string channel, bool overrideCheck = false); + Task JoinChannelAsync(string channel, bool overrideCheck = false); + /// /// Leaves the channel. /// /// The channel. - void LeaveChannel(JoinedChannel channel); + Task LeaveChannelAsync(JoinedChannel channel); + /// /// Leaves the channel. /// /// The channel. - void LeaveChannel(string channel); - + Task LeaveChannelAsync(string channel); + /// /// Called when [read line test]. /// /// The raw irc. - void OnReadLineTest(string rawIrc); + Task OnReadLineTestAsync(string rawIrc); /// /// Sends the message. @@ -332,14 +394,16 @@ public interface ITwitchClient /// The channel. /// The message. /// if set to true [dry run]. - void SendMessage(JoinedChannel channel, string message, bool dryRun = false); + Task SendMessageAsync(JoinedChannel channel, string message, bool dryRun = false); + /// /// Sends the message. /// /// The channel. /// The message. /// if set to true [dry run]. - void SendMessage(string channel, string message, bool dryRun = false); + Task SendMessageAsync(string channel, string message, bool dryRun = false); + /// /// Sends a formatted Twitch chat message reply. /// @@ -347,7 +411,8 @@ public interface ITwitchClient /// The message id that is being replied to /// Reply contents /// if set to true [dry run] - void SendReply(JoinedChannel channel, string replyToId, string message, bool dryRun = false); + Task SendReplyAsync(JoinedChannel channel, string replyToId, string message, bool dryRun = false); + /// /// SendReply wrapper that accepts channel in string form. /// @@ -355,23 +420,18 @@ public interface ITwitchClient /// The message id that is being replied to /// Reply contents /// if set to true [dry run] - void SendReply(string channel, string replyToId, string message, bool dryRun = false); + Task SendReplyAsync(string channel, string replyToId, string message, bool dryRun = false); + /// /// Sends the queued item. /// /// The message. - void SendQueuedItem(string message); + Task SendQueuedItemAsync(string message); + /// /// Sends the raw. /// /// The message. - void SendRaw(string message); - /// - /// Sends the whisper. - /// - /// The receiver. - /// The message. - /// if set to true [dry run]. - void SendWhisper(string receiver, string message, bool dryRun = false); + Task SendRawAsync(string message); } } diff --git a/TwitchLib.Client/Internal/Parsing/IrcParser.cs b/TwitchLib.Client/Internal/Parsing/IrcParser.cs deleted file mode 100644 index 4841a84a..00000000 --- a/TwitchLib.Client/Internal/Parsing/IrcParser.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Collections.Generic; -using TwitchLib.Client.Enums.Internal; -using TwitchLib.Client.Models.Internal; - -namespace TwitchLib.Client.Internal.Parsing -{ - /// - /// Class IrcParser. - /// - internal class IrcParser - { - - /// - /// Builds an IrcMessage from a raw string - /// - /// Raw IRC message - /// IrcMessage object - public IrcMessage ParseIrcMessage(string raw) - { - Dictionary tagDict = new Dictionary(); - - ParserState state = ParserState.STATE_NONE; - int[] starts = new[] { 0, 0, 0, 0, 0, 0 }; - int[] lens = new[] { 0, 0, 0, 0, 0, 0 }; - for (int i = 0; i < raw.Length; ++i) - { - lens[(int)state] = i - starts[(int)state] - 1; - if (state == ParserState.STATE_NONE && raw[i] == '@') - { - state = ParserState.STATE_V3; - starts[(int)state] = ++i; - - int start = i; - string key = null; - for (; i < raw.Length; ++i) - { - if (raw[i] == '=') - { - key = raw.Substring(start, i - start); - start = i + 1; - } - else if (raw[i] == ';') - { - if (key == null) - tagDict[raw.Substring(start, i - start)] = "1"; - else - tagDict[key] = raw.Substring(start, i - start); - start = i + 1; - } - else if (raw[i] == ' ') - { - if (key == null) - tagDict[raw.Substring(start, i - start)] = "1"; - else - tagDict[key] = raw.Substring(start, i - start); - break; - } - } - } - else if (state < ParserState.STATE_PREFIX && raw[i] == ':') - { - state = ParserState.STATE_PREFIX; - starts[(int)state] = ++i; - } - else if (state < ParserState.STATE_COMMAND) - { - state = ParserState.STATE_COMMAND; - starts[(int)state] = i; - } - else if (state < ParserState.STATE_TRAILING && raw[i] == ':') - { - state = ParserState.STATE_TRAILING; - starts[(int)state] = ++i; - break; - } - else if (state < ParserState.STATE_TRAILING && raw[i] == '+' || state < ParserState.STATE_TRAILING && raw[i] == '-') - { - state = ParserState.STATE_TRAILING; - starts[(int)state] = i; - break; - } - else if (state == ParserState.STATE_COMMAND) - { - state = ParserState.STATE_PARAM; - starts[(int)state] = i; - } - - while (i < raw.Length && raw[i] != ' ') - ++i; - } - - lens[(int)state] = raw.Length - starts[(int)state]; - string cmd = raw.Substring(starts[(int)ParserState.STATE_COMMAND], - lens[(int)ParserState.STATE_COMMAND]); - - IrcCommand command = IrcCommand.Unknown; - switch (cmd) - { - case "PRIVMSG": - command = IrcCommand.PrivMsg; - break; - case "NOTICE": - command = IrcCommand.Notice; - break; - case "PING": - command = IrcCommand.Ping; - break; - case "PONG": - command = IrcCommand.Pong; - break; - case "CLEARCHAT": - command = IrcCommand.ClearChat; - break; - case "CLEARMSG": - command = IrcCommand.ClearMsg; - break; - case "USERSTATE": - command = IrcCommand.UserState; - break; - case "GLOBALUSERSTATE": - command = IrcCommand.GlobalUserState; - break; - case "NICK": - command = IrcCommand.Nick; - break; - case "JOIN": - command = IrcCommand.Join; - break; - case "PART": - command = IrcCommand.Part; - break; - case "PASS": - command = IrcCommand.Pass; - break; - case "CAP": - command = IrcCommand.Cap; - break; - case "001": - command = IrcCommand.RPL_001; - break; - case "002": - command = IrcCommand.RPL_002; - break; - case "003": - command = IrcCommand.RPL_003; - break; - case "004": - command = IrcCommand.RPL_004; - break; - case "353": - command = IrcCommand.RPL_353; - break; - case "366": - command = IrcCommand.RPL_366; - break; - case "372": - command = IrcCommand.RPL_372; - break; - case "375": - command = IrcCommand.RPL_375; - break; - case "376": - command = IrcCommand.RPL_376; - break; - case "WHISPER": - command = IrcCommand.Whisper; - break; - case "SERVERCHANGE": - command = IrcCommand.ServerChange; - break; - case "RECONNECT": - command = IrcCommand.Reconnect; - break; - case "ROOMSTATE": - command = IrcCommand.RoomState; - break; - case "USERNOTICE": - command = IrcCommand.UserNotice; - break; - case "MODE": - command = IrcCommand.Mode; - break; - } - - string parameters = raw.Substring(starts[(int)ParserState.STATE_PARAM], - lens[(int)ParserState.STATE_PARAM]); - string message = raw.Substring(starts[(int)ParserState.STATE_TRAILING], - lens[(int)ParserState.STATE_TRAILING]); - string hostmask = raw.Substring(starts[(int)ParserState.STATE_PREFIX], - lens[(int)ParserState.STATE_PREFIX]); - return new IrcMessage(command, new[] { parameters, message }, hostmask, tagDict); - } - - /// - /// Enum ParserState - /// - private enum ParserState - { - /// - /// The state none - /// - STATE_NONE, - /// - /// The state v3 - /// - STATE_V3, - /// - /// The state prefix - /// - STATE_PREFIX, - /// - /// The state command - /// - STATE_COMMAND, - /// - /// The state parameter - /// - STATE_PARAM, - /// - /// The state trailing - /// - STATE_TRAILING - }; - } -} diff --git a/TwitchLib.Client/Internal/Rfc2812.cs b/TwitchLib.Client/Internal/Rfc2812.cs index c23906b9..12772256 100644 --- a/TwitchLib.Client/Internal/Rfc2812.cs +++ b/TwitchLib.Client/Internal/Rfc2812.cs @@ -1,5 +1,4 @@ -using System; -using System.Text; +using System.Text; using System.Text.RegularExpressions; namespace TwitchLib.Client.Internal @@ -7,7 +6,7 @@ namespace TwitchLib.Client.Internal /// /// Class detailing Rfc2812 specifications /// - public sealed class Rfc2812 + public static partial class Rfc2812 { // nickname = ( letter / special ) *8( letter / digit / special / "-" ) // letter = %x41-5A / %x61-7A ; A-Z / a-z @@ -17,14 +16,14 @@ public sealed class Rfc2812 /// /// The nickname regex /// - private static readonly Regex NicknameRegex = new Regex(@"^[A-Za-z\[\]\\`_^{|}][A-Za-z0-9\[\]\\`_\-^{|}]+$", RegexOptions.Compiled); +#if NET7_0_OR_GREATER + [GeneratedRegex(@"^[A-Za-z\[\]\\`_^{|}][A-Za-z0-9\[\]\\`_\-^{|}]+$")] + private static partial Regex GetNicknameRegex(); +#else + private static Regex GetNicknameRegex() => NicknameRegex; + private static readonly Regex NicknameRegex = new(@"^[A-Za-z\[\]\\`_^{|}][A-Za-z0-9\[\]\\`_\-^{|}]+$", RegexOptions.Compiled); +#endif - /// - /// Prevents a default instance of the class from being created. - /// - private Rfc2812() - { - } /// /// Checks if the passed nickname is valid according to the RFC @@ -35,7 +34,7 @@ private Rfc2812() public static bool IsValidNickname(string nickname) { return !string.IsNullOrEmpty(nickname) && - NicknameRegex.Match(nickname).Success; + GetNicknameRegex().Match(nickname).Success; } /// diff --git a/TwitchLib.Client/Manager/JoinedChannelManager.cs b/TwitchLib.Client/Manager/JoinedChannelManager.cs index 27d4790e..65962d48 100644 --- a/TwitchLib.Client/Manager/JoinedChannelManager.cs +++ b/TwitchLib.Client/Manager/JoinedChannelManager.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using TwitchLib.Client.Models; namespace TwitchLib.Client.Manager @@ -38,9 +35,9 @@ public void AddJoinedChannel(JoinedChannel joinedChannel) /// /// The channel. /// JoinedChannel. - public JoinedChannel GetJoinedChannel(string channel) + public JoinedChannel? GetJoinedChannel(string channel) { - _joinedChannels.TryGetValue(channel, out JoinedChannel joinedChannel); + _joinedChannels.TryGetValue(channel, out JoinedChannel? joinedChannel); return joinedChannel; } diff --git a/TwitchLib.Client/Parsing/IrcParser.cs b/TwitchLib.Client/Parsing/IrcParser.cs new file mode 100644 index 00000000..a7c66230 --- /dev/null +++ b/TwitchLib.Client/Parsing/IrcParser.cs @@ -0,0 +1,142 @@ +using TwitchLib.Client.Enums.Internal; +using TwitchLib.Client.Extensions; +using TwitchLib.Client.Models.Internal; + +namespace TwitchLib.Client.Parsing +{ + /// + /// Twitch IRCv3 message parser. + /// + public static class IrcParser + { + /// + /// Parses a raw Twitch IRCv3 message line into an IrcMessage object. + /// + /// Raw IRC message + /// that can be consumed by a variety of TwitchLib.Client types. + /// Thrown if the message is invalid. + /// Thrown if the message is invalid. + public static IrcMessage ParseMessage(string rawMessage) + { + if (rawMessage is not { Length: >= 3 }) + { + ThrowInvalidMessage(); + } + + // Sequentially parse each segment, advancing the span as we go. + // Adapted from https://github.com/neon-sunset/warpskimmer + var source = rawMessage.AsSpan(); + var tags = ParseTags(ref source); + var (user, hostmask) = ParsePrefix(ref source); + var command = ParseCommand(ref source); + + string parameters, message; + var messageOffset = source.IndexOf(" :".AsSpan()); + if (messageOffset >= 0) + { + parameters = source.Slice(0, messageOffset).ToString(); + message = source.Slice(messageOffset + 2).ToString(); + } + else + { + parameters = source.ToString(); + message = ""; + } + + return new IrcMessage(rawMessage, command, new[] { parameters, message }, user, hostmask, tags); + } + + private static Dictionary? ParseTags(ref ReadOnlySpan source) + { + var local = source; + if (local[0] != '@') + { + return null; + } + local = local.Slice(1); + + var tags = new Dictionary(); + + while (true) + { + var delimiter = local.IndexOfAny(';', ' '); + var (key, value) = local.Slice(0, delimiter).SplitFirst('='); + tags[key.ToString()] = value.ToString(); + + if (local[delimiter] is ' ') + { + source = local.Slice(delimiter + 1); + break; + } + + local = local.Slice(delimiter + 1); + } + + return tags; + } + + private static (string User, string Hostmask) ParsePrefix(ref ReadOnlySpan source) + { + var local = source; + if (local[0] != ':') + { + return default; + } + + var delimiter = local.IndexOf(' '); + var (user, hostmask) = local.Slice(1, delimiter - 1).SplitFirst('!'); + if (hostmask.IsEmpty) + { + hostmask = user; + } + + source = local.Slice(delimiter + 1); + return (user.ToString(), hostmask.ToString()); + } + + private static IrcCommand ParseCommand(ref ReadOnlySpan source) + { + (var command, source) = source.SplitFirst(' '); + + return command switch + { + "PRIVMSG" => IrcCommand.PrivMsg, + "NOTICE" => IrcCommand.Notice, + "PING" => IrcCommand.Ping, + "PONG" => IrcCommand.Pong, + "CLEARCHAT" => IrcCommand.ClearChat, + "CLEARMSG" => IrcCommand.ClearMsg, + "USERSTATE" => IrcCommand.UserState, + "GLOBALUSERSTATE" => IrcCommand.GlobalUserState, + "NICK" => IrcCommand.Nick, + "JOIN" => IrcCommand.Join, + "PART" => IrcCommand.Part, + "PASS" => IrcCommand.Pass, + "CAP" => IrcCommand.Cap, + "001" => IrcCommand.RPL_001, + "002" => IrcCommand.RPL_002, + "003" => IrcCommand.RPL_003, + "004" => IrcCommand.RPL_004, + "353" => IrcCommand.RPL_353, + "366" => IrcCommand.RPL_366, + "372" => IrcCommand.RPL_372, + "375" => IrcCommand.RPL_375, + "376" => IrcCommand.RPL_376, + "WHISPER" => IrcCommand.Whisper, + "SERVERCHANGE" => IrcCommand.ServerChange, + "RECONNECT" => IrcCommand.Reconnect, + "ROOMSTATE" => IrcCommand.RoomState, + "USERNOTICE" => IrcCommand.UserNotice, + "MODE" => IrcCommand.Mode, + _ => IrcCommand.Unknown, + }; + } + + // Not throwing message contents because it may contain sensitive information. + private static void ThrowInvalidMessage() + { + throw new FormatException( + "Unexpected IRC message format: must be not null and longer than 3 characters."); + } + } +} diff --git a/TwitchLib.Client/Throttling/Throttler.cs b/TwitchLib.Client/Throttling/Throttler.cs new file mode 100644 index 00000000..1634e10a --- /dev/null +++ b/TwitchLib.Client/Throttling/Throttler.cs @@ -0,0 +1,51 @@ +using System; +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Interfaces; + +namespace TwitchLib.Client.Throttling +{ + internal class Throttler + { + private readonly ISendOptions _sendOptions; + private DateTime? _firstMessage; + private uint _sentItemCount; + + internal Throttler(ISendOptions sendOptions) + { + _sendOptions = sendOptions ?? new SendOptions(); + } + + /// + /// , if the current item may be sent + /// , if the current item has to be throttled/may not be sent + /// + public bool ShouldThrottle() + { + if (IsThrottlingPeriodExceeded()) + { + Reset(); + } + + // no else-if to catch `this.SendOptions.SendsAllowedInPeriod = 0` + if (_sentItemCount >= _sendOptions.SendsAllowedInPeriod) + { + return true; + } + + _sentItemCount++; + return false; + } + + private void Reset() + { + _firstMessage = DateTime.UtcNow; + _sentItemCount = 0; + } + + private bool IsThrottlingPeriodExceeded() + { + return _firstMessage == null || + DateTime.UtcNow.Subtract(_firstMessage.Value) > _sendOptions.ThrottlingPeriod; + } + } +} \ No newline at end of file diff --git a/TwitchLib.Client/Throttling/ThrottlingService.cs b/TwitchLib.Client/Throttling/ThrottlingService.cs new file mode 100644 index 00000000..d3675c1e --- /dev/null +++ b/TwitchLib.Client/Throttling/ThrottlingService.cs @@ -0,0 +1,154 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using TwitchLib.Client.Events; +using TwitchLib.Client.Extensions; +using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Interfaces; +using TwitchLib.Communication.Events; +using TwitchLib.Communication.Interfaces; + +namespace TwitchLib.Client.Throttling +{ + internal class ThrottlingService + { + private readonly IClient _client; + private readonly ISendOptions _sendOptions; + private readonly ILogger? _logger; + private readonly ConcurrentQueue<(DateTime, OutboundChatMessage)> _queue = new ConcurrentQueue<(DateTime, OutboundChatMessage)>(); + private readonly Throttler _throttler; + private CancellationTokenSource _tokenSource = new CancellationTokenSource(); + private Task? _sendTask; + + internal event AsyncEventHandler? OnThrottled; + internal event AsyncEventHandler? OnError; + + internal ThrottlingService( + IClient client, + ISendOptions messageSendOptions, + ILogger? logger = null) + { + _logger = logger; + _client = client; + _sendOptions = messageSendOptions ?? new SendOptions(); + _throttler = new Throttler(_sendOptions); + + _client.OnConnected -= StartThrottlerAsync; + _client.OnConnected += StartThrottlerAsync; + + _client.OnReconnected -= StartThrottlerAsync; + _client.OnReconnected += StartThrottlerAsync; + + _client.OnDisconnected -= StopThrottlerAsync; + _client.OnDisconnected += StopThrottlerAsync; + } + + /// + /// enqueues the given + /// + /// + /// + /// + /// + /// , if its enqueued + ///

+ /// otherwise + ///
+ internal bool Enqueue(OutboundChatMessage? message) + { + if (!_client.IsConnected || + _queue.Count >= _sendOptions.QueueCapacity || + message == null) + { + return false; + } + + _queue.Enqueue((DateTime.UtcNow, message)); + return true; + } + + private Task StartThrottlerAsync(object? sender, Communication.Events.OnConnectedEventArgs args) + { + // Cancel old token first + _tokenSource.Cancel(); + _tokenSource = new CancellationTokenSource(); + _sendTask = Task.Run(async () => await SendTaskActionAsync(), _tokenSource.Token); + return Task.CompletedTask; + } + + private Task StopThrottlerAsync(object? sender, OnDisconnectedEventArgs args) + { + _tokenSource.Cancel(); + return Task.CompletedTask; + } + + private async Task SendTaskActionAsync() + { + while (_client.IsConnected && !_tokenSource.Token.IsCancellationRequested) + { + await TrySendAsync(); + await Task.Delay(_sendOptions.SendDelay); + } + } + + private async Task TrySendAsync() + { + try + { + var taken = _queue.TryDequeue(out var message); + // 'message == default' should never happen, but if it does, the check will + // work correctly due to default state of DateTime and OutboundChatMessage being null. + if (!taken || message == default) + { + return; + } + + // Check CacheItemTimeout + if (message.Item1.Add(_sendOptions.CacheItemTimeout) < DateTime.UtcNow) + { + return; + } + + // Check for throttling + // If the consumer of this API passes zero for SendsAllowedInPeriod + // to the ctor of SendOptions + // this Sequence-order makes it transparent + // cause Throttle raises the corresponding Event with the needed information. + if (_throttler.ShouldThrottle()) + { + await ThrottleMessageAsync(message.Item2); + return; + } + + var messageToSend = message.Item2.ToString(); + await _client.SendAsync(messageToSend); + } + catch (TaskCanceledException) + { + // NOOP + } + catch (OperationCanceledException) + { + // NOOP + } + catch (Exception ex) + { + _logger?.LogException(ex.Message, ex); + + await OnError.TryInvoke(this, new OnErrorEventArgs(ex)); + } + } + + private Task ThrottleMessageAsync(OutboundChatMessage itemNotSent) + { + const string msg = "Message Throttle Occured. Too Many Messages within the period specified in ISendOptions."; + + var args = new OnMessageThrottledArgs( + msg, + itemNotSent, + _sendOptions.ThrottlingPeriod, + _sendOptions.SendsAllowedInPeriod); + + return OnThrottled.TryInvoke(null, args); + } + } +} \ No newline at end of file diff --git a/TwitchLib.Client/TwitchClient.cs b/TwitchLib.Client/TwitchClient.cs index 72e0ab1c..85b0fa57 100644 --- a/TwitchLib.Client/TwitchClient.cs +++ b/TwitchLib.Client/TwitchClient.cs @@ -1,22 +1,24 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Microsoft.Extensions.Logging; using TwitchLib.Client.Enums; using TwitchLib.Client.Enums.Internal; using TwitchLib.Client.Events; using TwitchLib.Client.Exceptions; +using TwitchLib.Client.Extensions; using TwitchLib.Client.Interfaces; using TwitchLib.Client.Internal; -using TwitchLib.Client.Internal.Parsing; +using TwitchLib.Client.Parsing; using TwitchLib.Client.Manager; using TwitchLib.Client.Models; +using TwitchLib.Client.Models.Interfaces; using TwitchLib.Client.Models.Internal; +using TwitchLib.Client.Throttling; using TwitchLib.Communication.Clients; using TwitchLib.Communication.Events; using TwitchLib.Communication.Interfaces; +using TwitchLib.Client.Models.Extensions; namespace TwitchLib.Client { @@ -31,418 +33,284 @@ public class TwitchClient : ITwitchClient /// /// The client /// - private IClient _client; - /// - /// The channel emotes - /// - private MessageEmoteCollection _channelEmotes = new MessageEmoteCollection(); - /// - /// The chat command identifiers - /// - private readonly ICollection _chatCommandIdentifiers = new HashSet(); - /// - /// The whisper command identifiers - /// - private readonly ICollection _whisperCommandIdentifiers = new HashSet(); + private IClient? _client; + + private readonly ISendOptions _sendOptions; + private ThrottlingService? _throttling; + /// /// The join channel queue /// private readonly Queue _joinChannelQueue = new Queue(); + /// /// The logger /// - private readonly ILogger _logger; + private readonly ILogger? _logger; + + private readonly ILoggerFactory? _loggerFactory; + /// /// The protocol /// private readonly ClientProtocol _protocol; + /// /// The currently joining channels /// private bool _currentlyJoiningChannels; + /// /// The join timer /// - private System.Timers.Timer _joinTimer; + private System.Timers.Timer? _joinTimer; + /// /// The awaiting joins /// - private List> _awaitingJoins; + private readonly List> _awaitingJoins = new(); - /// - /// The irc parser - /// - private readonly IrcParser _ircParser; /// /// The joined channel manager /// - private readonly JoinedChannelManager _joinedChannelManager; + private readonly JoinedChannelManager _joinedChannelManager = new(); // variables used for constructing OnMessageSent properties /// /// The has seen joined channels /// - private readonly List _hasSeenJoinedChannels = new List(); + private readonly HashSet _hasSeenJoinedChannels = new HashSet(); + /// /// The last message sent /// - private string _lastMessageSent; + private string _lastMessageSent = string.Empty; #endregion #region Public Variables - /// - /// Assembly version of TwitchLib.Client. - /// - /// The version. - public Version Version => Assembly.GetEntryAssembly().GetName().Version; - /// - /// Checks if underlying client has been initialized. - /// - /// true if this instance is initialized; otherwise, false. + /// + public ICollection ChatCommandIdentifiers { get; } = new HashSet(); + + /// + public ICollection WhisperCommandIdentifiers { get; } = new HashSet(); + + /// +#if NET + [MemberNotNullWhen(true, nameof(_client))] +#endif public bool IsInitialized => _client != null; - /// - /// A list of all channels the client is currently in. - /// - /// The joined channels. + + /// public IReadOnlyList JoinedChannels => _joinedChannelManager.GetJoinedChannels(); - /// - /// Username of the user connected via this library. - /// - /// The twitch username. - public string TwitchUsername { get; private set; } - /// - /// The most recent whisper received. - /// - /// The previous whisper. - public WhisperMessage PreviousWhisper { get; private set; } - /// - /// The current connection status of the client. - /// - /// true if this instance is connected; otherwise, false. - public bool IsConnected => IsInitialized && _client != null ? _client.IsConnected : false; - /// - /// The emotes this channel replaces. - /// - /// The channel emotes. - /// Twitch-handled emotes are automatically added to this collection (which also accounts for - /// managing user emote permissions such as sub-only emotes). Third-party emotes will have to be manually - /// added according to the availability rules defined by the third-party. - public MessageEmoteCollection ChannelEmotes => _channelEmotes; + /// + public string TwitchUsername => ConnectionCredentials?.TwitchUsername ?? string.Empty; - /// - /// Will disable the client from sending automatic PONG responses to PING - /// - /// true if [disable automatic pong]; otherwise, false. - public bool DisableAutoPong { get; set; } = false; - /// - /// Determines whether Emotes will be replaced in messages. - /// - /// true if [will replace emotes]; otherwise, false. - public bool WillReplaceEmotes { get; set; } = false; - /// - /// Provides access to connection credentials object. - /// - /// The connection credentials. - public ConnectionCredentials ConnectionCredentials { get; private set; } - /// - /// Provides access to autorelistiononexception on off boolean. - /// - /// true if [automatic re listen on exception]; otherwise, false. - public bool AutoReListenOnException { get; set; } + /// + public WhisperMessage? PreviousWhisper { get; private set; } - #endregion + /// + public bool IsConnected => IsInitialized && _client.IsConnected == true; - #region Events - /// - /// Fires when an Announcement is received - /// - public event EventHandler OnAnnouncement; + /// + /// + /// Twitch-handled emotes are automatically added to this collection (which also accounts for + /// managing user emote permissions such as sub-only emotes). Third-party emotes will have to be manually + /// added according to the availability rules defined by the third-party. + /// + public MessageEmoteCollection ChannelEmotes { get; } = new MessageEmoteCollection(); - /// - /// Fires when VIPs are received from chat - /// - public event EventHandler OnVIPsReceived; + /// + public bool DisableAutoPong { get; set; } = false; - /// - /// Fires whenever a log write happens. - /// - public event EventHandler OnLog; + /// + public bool WillReplaceEmotes { get; set; } = false; /// - /// Fires when client connects to Twitch. + /// Adds to replaced Emotes their prefix. Defaults to empty string. /// - public event EventHandler OnConnected; + public string ReplacedEmotesPrefix { get; set; } = ""; /// - /// Fires when client joins a channel. + /// Adds to replaced Emotes their suffix. Defaults to empty string. /// - public event EventHandler OnJoinedChannel; + public string ReplacedEmotesSuffix { get; set; } = ""; - /// - /// Fires on logging in with incorrect details, returns ErrorLoggingInException. - /// - public event EventHandler OnIncorrectLogin; + /// + public ConnectionCredentials? ConnectionCredentials { get; private set; } + #endregion - /// - /// Fires when connecting and channel state is changed, returns ChannelState. - /// - public event EventHandler OnChannelStateChanged; + #region Events + /// + public event AsyncEventHandler? OnAnnouncement; - /// - /// Fires when a user state is received, returns UserState. - /// - public event EventHandler OnUserStateChanged; + /// + public event AsyncEventHandler? OnConnected; - /// - /// Fires when a new chat message arrives, returns ChatMessage. - /// - public event EventHandler OnMessageReceived; + /// + public event AsyncEventHandler? OnJoinedChannel; - /// - /// Fires when a new whisper arrives, returns WhisperMessage. - /// - public event EventHandler OnWhisperReceived; + /// + public event AsyncEventHandler? OnIncorrectLogin; - /// - /// Fires when a chat message is sent, returns username, channel and message. - /// - public event EventHandler OnMessageSent; + /// + public event AsyncEventHandler? OnChannelStateChanged; - /// - /// Fires when a whisper message is sent, returns username and message. - /// - public event EventHandler OnWhisperSent; + /// + public event AsyncEventHandler? OnUserStateChanged; - /// - /// Fires when command (uses custom chat command identifier) is received, returns channel, command, ChatMessage, arguments as string, arguments as list. - /// - public event EventHandler OnChatCommandReceived; + /// + public event AsyncEventHandler? OnMessageReceived; - /// - /// Fires when command (uses custom whisper command identifier) is received, returns command, Whispermessage. - /// - public event EventHandler OnWhisperCommandReceived; + /// + public event AsyncEventHandler? OnWhisperReceived; - /// - /// Fires when a new viewer/chatter joined the channel's chat room, returns username and channel. - /// - public event EventHandler OnUserJoined; + /// + public event AsyncEventHandler? OnMessageSent; - /// - /// Fires when a moderator joined the channel's chat room, returns username and channel. - /// - public event EventHandler OnModeratorJoined; + /// + public event AsyncEventHandler? OnChatCommandReceived; - /// - /// Fires when a moderator joins the channel's chat room, returns username and channel. - /// - public event EventHandler OnModeratorLeft; + /// + public event AsyncEventHandler? OnWhisperCommandReceived; - /// - /// Fires when a message gets deleted in chat. - /// - public event EventHandler OnMessageCleared; + /// + public event AsyncEventHandler? OnUserJoined; - /// - /// Fires when new subscriber is announced in chat, returns Subscriber. - /// - public event EventHandler OnNewSubscriber; + /// + public event AsyncEventHandler? OnMessageCleared; - /// - /// Fires when current subscriber renews subscription, returns ReSubscriber. - /// - public event EventHandler OnReSubscriber; + /// + public event AsyncEventHandler? OnNewSubscriber; - /// - /// Fires when a current Prime gaming subscriber converts to a paid subscription. - /// - public event EventHandler OnPrimePaidSubscriber; + /// + public event AsyncEventHandler? OnReSubscriber; - /// - /// Fires when Twitch notifies client of existing users in chat. - /// - public event EventHandler OnExistingUsersDetected; + /// + public event AsyncEventHandler? OnPrimePaidSubscriber; - /// - /// Fires when a PART message is received from Twitch regarding a particular viewer - /// - public event EventHandler OnUserLeft; + /// + public event AsyncEventHandler? OnExistingUsersDetected; - /// - /// Fires when bot has disconnected. - /// - public event EventHandler OnDisconnected; + /// + public event AsyncEventHandler? OnUserLeft; - /// - /// Forces when bot suffers connection error. - /// - public event EventHandler OnConnectionError; + /// + public event AsyncEventHandler? OnDisconnected; - /// - /// Fires when a channel's chat is cleared. - /// - public event EventHandler OnChatCleared; + /// + public event AsyncEventHandler? OnConnectionError; - /// - /// Fires when a viewer gets timedout by any moderator. - /// - public event EventHandler OnUserTimedout; + /// + public event AsyncEventHandler? OnChatCleared; - /// - /// Fires when client successfully leaves a channel. - /// - public event EventHandler OnLeftChannel; + /// + public event AsyncEventHandler? OnUserTimedout; - /// - /// Fires when a viewer gets banned by any moderator. - /// - public event EventHandler OnUserBanned; + /// + public event AsyncEventHandler? OnLeftChannel; - /// - /// Fires when a list of moderators is received. - /// - public event EventHandler OnModeratorsReceived; + /// + public event AsyncEventHandler? OnUserBanned; - /// - /// Fires when confirmation of a chat color change request was received. - /// - public event EventHandler OnChatColorChanged; + /// + public event AsyncEventHandler? OnSendReceiveData; - /// - /// Fires when data is either received or sent. - /// - public event EventHandler OnSendReceiveData; + /// + public event AsyncEventHandler? OnRaidNotification; - /// - /// Fires when a raid notification is detected in chat - /// - public event EventHandler OnRaidNotification; + /// + public event AsyncEventHandler? OnGiftedSubscription; - /// - /// Fires when a subscription is gifted and announced in chat - /// - public event EventHandler OnGiftedSubscription; + /// + public event AsyncEventHandler? OnCommunitySubscription; - /// - /// Fires when a community subscription is announced in chat - /// - public event EventHandler OnCommunitySubscription; + /// + public event AsyncEventHandler? OnContinuedGiftedSubscription; - /// - /// - /// - public event EventHandler OnContinuedGiftedSubscription; + /// + public event AsyncEventHandler? OnMessageThrottled; - /// - /// Fires when a Message has been throttled. - /// - public event EventHandler OnMessageThrottled; + /// + public event AsyncEventHandler? OnError; - /// - /// Fires when a Whisper has been throttled. - /// - public event EventHandler OnWhisperThrottled; + /// + public event AsyncEventHandler? OnReconnected; - /// - /// Occurs when an Error is thrown in the protocol client - /// - public event EventHandler OnError; + /// + public event AsyncEventHandler? OnRequiresVerifiedEmail; - /// - /// Occurs when a reconnection occurs. - /// - public event EventHandler OnReconnected; + /// + public event AsyncEventHandler? OnRequiresVerifiedPhoneNumber; - /// - /// Occurs when chatting in a channel that requires a verified email without a verified email attached to the account. - /// - public event EventHandler OnRequiresVerifiedEmail; + /// + public event AsyncEventHandler? OnRateLimit; - /// - /// Occurs when chatting in a channel that requires a verified phone number without a verified phone number attached to the account. - /// - public event EventHandler OnRequiresVerifiedPhoneNumber; + /// + public event AsyncEventHandler? OnDuplicate; - /// - /// Occurs when send message rate limit has been applied to the client in a specific channel by Twitch - /// - public event EventHandler OnRateLimit; + /// + public event AsyncEventHandler? OnBannedEmailAlias; - /// - /// Occurs when sending duplicate messages and user is not permitted to do so - /// - public event EventHandler OnDuplicate; + /// + public event AsyncEventHandler? OnSelfRaidError; - /// - /// Occurs when chatting in a channel that the user is banned in bcs of an already banned alias with the same Email - /// - public event EventHandler OnBannedEmailAlias; + /// + public event AsyncEventHandler? OnNoPermissionError; - /// - /// Fires when TwitchClient attempts to host a channel it is in. - /// - public event EventHandler OnSelfRaidError; + /// + public event AsyncEventHandler? OnRaidedChannelIsMatureAudience; - /// - /// Fires when TwitchClient receives generic no permission error from Twitch. - /// - public event EventHandler OnNoPermissionError; + /// + public event AsyncEventHandler? OnFailureToReceiveJoinConfirmation; - /// - /// Fires when newly raided channel is mature audience only. - /// - public event EventHandler OnRaidedChannelIsMatureAudience; + /// + public event AsyncEventHandler? OnFollowersOnly; - /// - /// Fires when the client was unable to join a channel. - /// - public event EventHandler OnFailureToReceiveJoinConfirmation; + /// + public event AsyncEventHandler? OnSubsOnly; - /// - /// Fires when the client attempts to send a message to a channel in followers only mode, as a non-follower - /// - public event EventHandler OnFollowersOnly; + /// + public event AsyncEventHandler? OnEmoteOnly; - /// - /// Fires when the client attempts to send a message to a channel in subs only mode, as a non-sub - /// - public event EventHandler OnSubsOnly; + /// + public event AsyncEventHandler? OnSuspended; - /// - /// Fires when the client attempts to send a non-emote message to a channel in emotes only mode - /// - public event EventHandler OnEmoteOnly; + /// + public event AsyncEventHandler? OnBanned; - /// - /// Fires when the client attempts to send a message to a channel that has been suspended - /// - public event EventHandler OnSuspended; + /// + public event AsyncEventHandler? OnSlowMode; - /// - /// Fires when the client attempts to send a message to a channel they're banned in - /// - public event EventHandler OnBanned; + /// + public event AsyncEventHandler? OnSlowModeChannel; - /// - /// Fires when the client attempts to send a message in a channel with slow mode enabled, without cooldown expiring - /// - public event EventHandler OnSlowMode; + /// + public event AsyncEventHandler? OnR9kMode; - /// - /// Fires when the client attempts to send a message in a channel with r9k mode enabled, and message was not permitted - /// - public event EventHandler OnR9kMode; + /// + public event AsyncEventHandler? OnUserIntro; - /// - /// Fires when the client receives a PRIVMSG tagged as an user-intro - /// - public event EventHandler OnUserIntro; + /// + public event AsyncEventHandler? OnUnaccountedFor; - /// - /// Fires when data is received from Twitch that is not able to be parsed. - /// - public event EventHandler OnUnaccountedFor; + /// + public event AsyncEventHandler? OnAnonGiftPaidUpgrade; + + /// + public event AsyncEventHandler? OnUnraidNotification; + + /// + public event AsyncEventHandler? OnRitual; + + /// + public event AsyncEventHandler? OnBitsBadgeTier; + + /// + public event AsyncEventHandler? OnCommunityPayForward; + + /// + public event AsyncEventHandler? OnStandardPayForward; #endregion #region Construction Work @@ -452,76 +320,73 @@ public class TwitchClient : ITwitchClient ///
/// Protocol Client to use for connection from TwitchLib.Communication. Possible Options Are the TcpClient client or WebSocket client. /// The protocol. - /// Optional ILogger instance to enable logging - public TwitchClient(IClient client = null, ClientProtocol protocol = ClientProtocol.WebSocket, ILogger logger = null) + /// Send options with throttling settings. + /// Optional ILoggerFactory instance to enable logging + public TwitchClient( + IClient? client = null, + ClientProtocol protocol = ClientProtocol.WebSocket, + ISendOptions? sendOptions = null, + ILoggerFactory? loggerFactory = null) { - _logger = logger; + _loggerFactory = loggerFactory; + _logger = loggerFactory?.CreateLogger(); _client = client; _protocol = protocol; - _joinedChannelManager = new JoinedChannelManager(); - _ircParser = new IrcParser(); + _sendOptions = sendOptions ?? new SendOptions(); } - /// - /// Initializes the TwitchChatClient class. - /// - /// The credentials to use to log in. - /// The channel to connect to. - /// The identifier to be used for reading and writing commands from chat. - /// The identifier to be used for reading and writing commands from whispers. - /// By default, TwitchClient will silence exceptions and auto-relisten for overall stability. For debugging, you may wish to have the exception bubble up, set this to false. - public void Initialize(ConnectionCredentials credentials, string channel = null, char chatCommandIdentifier = '!', char whisperCommandIdentifier = '!', bool autoReListenOnExceptions = true) + /// + /// + /// If or dont have any command identifier the '!' is added. + /// + public void Initialize(ConnectionCredentials credentials, string? channel = null) { - if (channel != null && channel[0] == '#') channel = channel.Substring(1); - initializeHelper(credentials, new List() { channel }, chatCommandIdentifier, whisperCommandIdentifier, autoReListenOnExceptions); + var channels = new List(); + if(channel is not null) + channels.Add(channel); + Initialize(credentials, channels); } - /// - /// Initializes the TwitchChatClient class (with multiple channels). - /// - /// The credentials to use to log in. - /// List of channels to join when connected - /// The identifier to be used for reading and writing commands from chat. - /// The identifier to be used for reading and writing commands from whispers. - /// By default, TwitchClient will silence exceptions and auto-relisten for overall stability. For debugging, you may wish to have the exception bubble up, set this to false. - public void Initialize(ConnectionCredentials credentials, List channels, char chatCommandIdentifier = '!', char whisperCommandIdentifier = '!', bool autoReListenOnExceptions = true) + /// + /// + /// If or dont have any command identifier the '!' is added. + /// + public void Initialize(ConnectionCredentials credentials, List channels) { - channels = channels.Select(x => x[0] == '#' ? x.Substring(1) : x).ToList(); - initializeHelper(credentials, channels, chatCommandIdentifier, whisperCommandIdentifier, autoReListenOnExceptions); + channels = channels.ConvertAll(x => x.StartsWith("#") ? x.Substring(1) : x); + InitializationHelper(credentials, channels); } /// /// Runs initialization logic that is shared by the overriden Initialize methods. /// + /// + /// If or dont have any command identifier the '!' is added. + /// /// The credentials to use to log in. /// List of channels to join when connected - /// The identifier to be used for reading and writing commands from chat. - /// The identifier to be used for reading and writing commands from whispers. - /// By default, TwitchClient will silence exceptions and auto-relisten for overall stability. For debugging, you may wish to have the exception bubble up, set this to false. - private void initializeHelper(ConnectionCredentials credentials, List channels, char chatCommandIdentifier = '!', char whisperCommandIdentifier = '!', bool autoReListenOnExceptions = true) + private void InitializationHelper( + ConnectionCredentials credentials, + List channels) { - Log($"TwitchLib-TwitchClient initialized, assembly version: {Assembly.GetExecutingAssembly().GetName().Version}", level: LogLevel.Information); + _logger?.LogInitialized(Assembly.GetExecutingAssembly().GetName().Version!); ConnectionCredentials = credentials; - TwitchUsername = ConnectionCredentials.TwitchUsername; - if (chatCommandIdentifier != '\0') - _chatCommandIdentifiers.Add(chatCommandIdentifier); - if (whisperCommandIdentifier != '\0') - _whisperCommandIdentifiers.Add(whisperCommandIdentifier); - AutoReListenOnException = autoReListenOnExceptions; + if (ChatCommandIdentifiers.Count == 0) + ChatCommandIdentifiers.Add("!"); + if (WhisperCommandIdentifiers.Count == 0) + WhisperCommandIdentifiers.Add("!"); - if (channels != null && channels.Count > 0) + for (var i = 0; i < channels.Count; i++) { - for(var i = 0; i < channels.Count; i++) - { - if (string.IsNullOrEmpty(channels[i])) - continue; + if (string.IsNullOrEmpty(channels[i])) + continue; - // Check to see if client is already in channel - if (JoinedChannels.FirstOrDefault(x => x.Channel.ToLower() == channels[i]) != null) - return; - _joinChannelQueue.Enqueue(new JoinedChannel(channels[i])); - } + // Check to see if client is already in channel + if (JoinedChannels.Any(x => x.Channel.Equals(channels[i], StringComparison.OrdinalIgnoreCase))) + return; + + _joinChannelQueue.Enqueue(new JoinedChannel(channels[i])); } InitializeClient(); @@ -532,259 +397,143 @@ private void initializeHelper(ConnectionCredentials credentials, List ch ///
private void InitializeClient() { - if (_client == null) + _client ??= _protocol switch { - switch (_protocol) - { - case ClientProtocol.TCP: - _client = new TcpClient(); - break; - case ClientProtocol.WebSocket: - _client = new WebSocketClient(); - break; - } - } + ClientProtocol.TCP => new TcpClient(null, _loggerFactory?.CreateLogger()), + ClientProtocol.WebSocket => new WebSocketClient(null, _loggerFactory?.CreateLogger()), + _ => throw new ArgumentOutOfRangeException(nameof(_protocol), _protocol, null) + }; Debug.Assert(_client != null, nameof(_client) + " != null"); - _client.OnConnected += _client_OnConnected; + _throttling = new ThrottlingService(_client, _sendOptions, _logger); + _throttling.OnThrottled += OnThrottled; + _throttling.OnError += ThrottlerOnError; + + _client.OnConnected += _client_OnConnectedAsync; _client.OnMessage += _client_OnMessage; _client.OnDisconnected += _client_OnDisconnected; _client.OnFatality += _client_OnFatality; - _client.OnMessageThrottled += _client_OnMessageThrottled; - _client.OnWhisperThrottled += _client_OnWhisperThrottled; _client.OnReconnected += _client_OnReconnected; } - #endregion - /// - /// Raises the event. - /// - /// Name of the event. - /// The arguments. - internal void RaiseEvent(string eventName, object args = null) - { - FieldInfo fInfo = GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic) as FieldInfo; - MulticastDelegate multi = fInfo.GetValue(this) as MulticastDelegate; - foreach (Delegate del in multi.GetInvocationList()) - { - del.Method.Invoke(del.Target, args == null ? new object[] { this, new EventArgs() } : new[] { this, args }); - } - } - - /// - /// Sends a RAW IRC message. - /// - /// The RAW message to be sent. - public void SendRaw(string message) + /// + public async Task SendRawAsync(string message) { - if (!IsInitialized) HandleNotInitialized(); + if (!IsInitialized) + HandleNotInitialized(); - Log($"Writing: {message}"); - _client.Send(message); - OnSendReceiveData?.Invoke(this, new OnSendReceiveDataArgs { Direction = Enums.SendReceiveDirection.Sent, Data = message }); + _logger?.LogWriting(message); + await _client.SendAsync(message); + await OnSendReceiveData.TryInvoke(this, new(SendReceiveDirection.Sent, message)); } #region SendMessage - private void SendTwitchMessage(JoinedChannel channel, string message, string replyToId = null, bool dryRun = false) + private void SendTwitchMessage(JoinedChannel? channel, string? message, string? replyToId = null, bool dryRun = false) { - if (!IsInitialized) HandleNotInitialized(); - if (channel == null || message == null || dryRun) return; + if (!IsInitialized) + HandleNotInitialized(); + + if (channel == null || message == null || dryRun) + return; + if (message.Length > 500) { - LogError("Message length has exceeded the maximum character count. (500)"); + _logger?.LogMessageTooLong(); return; } - OutboundChatMessage twitchMessage = new OutboundChatMessage - { - Channel = channel.Channel, - Username = ConnectionCredentials.TwitchUsername, - Message = message - }; - if(replyToId != null) + var twitchMessage = new OutboundChatMessage(channel.Channel, message); + + if (replyToId != null) { twitchMessage.ReplyToId = replyToId; } _lastMessageSent = message; - - - _client.Send(twitchMessage.ToString()); + _throttling!.Enqueue(twitchMessage); } - /// - /// Sends a formatted Twitch channel chat message. - /// - /// Channel to send message to. - /// The message to be sent. - /// If set to true, the message will not actually be sent for testing purposes. - public void SendMessage(JoinedChannel channel, string message, bool dryRun = false) + /// + public Task SendMessageAsync(JoinedChannel channel, string message, bool dryRun = false) { - SendTwitchMessage(channel, message, null, dryRun); + SendTwitchMessage(channel, message,null, dryRun); + return Task.CompletedTask; } - /// - /// SendMessage wrapper that accepts channel in string form. - /// - /// The channel. - /// The message. - /// if set to true [dry run]. - public void SendMessage(string channel, string message, bool dryRun = false) + /// + public Task SendMessageAsync(string channel, string message, bool dryRun = false) { - SendMessage(GetJoinedChannel(channel), message, dryRun); + return SendMessageAsync(GetJoinedChannel(channel), message, dryRun); } - /// - /// Sends a formatted Twitch chat message reply. - /// - /// Channel to send Twitch chat reply to - /// The message id that is being replied to - /// Reply contents - /// if set to true [dry run] - public void SendReply(JoinedChannel channel, string replyToId, string message, bool dryRun = false) + /// + public Task SendReplyAsync(JoinedChannel channel, string replyToId, string message, bool dryRun = false) { SendTwitchMessage(channel, message, replyToId, dryRun); + return Task.CompletedTask; } - /// - /// SendReply wrapper that accepts channel in string form. - /// - /// Channel to send Twitch chat reply to - /// The message id that is being replied to - /// Reply contents - /// if set to true [dry run] - public void SendReply(string channel, string replyToId, string message, bool dryRun = false) + /// + public Task SendReplyAsync(string channel, string replyToId, string message, bool dryRun = false) { - SendReply(GetJoinedChannel(channel), replyToId, message, dryRun); - } - - #endregion - - #region Whispers - /// - /// Sends a formatted whisper message to someone. - /// - /// The receiver of the whisper. - /// The message to be sent. - /// If set to true, the message will not actually be sent for testing purposes. - [Obsolete("Usage of this command through chat is not possible anymore. Use TwitchLib.Api.Helix.Whispers.SendWhisperAsync() instead.")] - public void SendWhisper(string receiver, string message, bool dryRun = false) - { - if (!IsInitialized) HandleNotInitialized(); - if (dryRun) return; - - OutboundWhisperMessage twitchMessage = new OutboundWhisperMessage - { - Receiver = receiver, - Username = ConnectionCredentials.TwitchUsername, - Message = message - }; - - _client.SendWhisper(twitchMessage.ToString()); - - OnWhisperSent?.Invoke(this, new OnWhisperSentArgs { Receiver = receiver, Message = message }); + return SendReplyAsync(GetJoinedChannel(channel), replyToId, message, dryRun); } #endregion #region Connection Calls - /// - /// Start connecting to the Twitch IRC chat. - /// - /// bool representing Connect() result - public bool Connect() + + /// + public async Task ConnectAsync() { - if (!IsInitialized) HandleNotInitialized(); - Log($"Connecting to: {ConnectionCredentials.TwitchWebsocketURI}", level: LogLevel.Information); + if (!IsInitialized) + HandleNotInitialized(); + + _logger?.LogConnecting(); - // Clear instance data + // Clear instance data _joinedChannelManager.Clear(); - if(_client.Open()) + if (await _client.OpenAsync()) { - Log("Should be connected!"); + _logger?.LogShouldBeConnected(); return true; } + return false; } - /// - /// Start disconnecting from the Twitch IRC chat. - /// - public void Disconnect() + /// + public async Task DisconnectAsync() { - Log("Disconnecting Twitch Chat Client...", level: LogLevel.Information); + _logger?.LogDisconnecting(); + + if (!IsInitialized) + HandleNotInitialized(); - if (!IsInitialized) HandleNotInitialized(); - _client.Close(); + await _client.CloseAsync(); // Clear instance data _joinedChannelManager.Clear(); PreviousWhisper = null; } - /// - /// Start reconnecting to the Twitch IRC chat. - /// - public void Reconnect() - { - if (!IsInitialized) HandleNotInitialized(); - Log($"Reconnecting to Twitch", level: LogLevel.Information); - _client.Reconnect(); - } - #endregion - - #region Command Identifiers - /// - /// Adds a character to a list of characters that if found at the start of a message, fires command received event. - /// - /// Character, that if found at start of message, fires command received event. - public void AddChatCommandIdentifier(char identifier) - { - if (!IsInitialized) HandleNotInitialized(); - _chatCommandIdentifiers.Add(identifier); - } - - /// - /// Removes a character from a list of characters that if found at the start of a message, fires command received event. - /// - /// Command identifier to removed from identifier list. - public void RemoveChatCommandIdentifier(char identifier) - { - if (!IsInitialized) HandleNotInitialized(); - _chatCommandIdentifiers.Remove(identifier); - } - - /// - /// Adds a character to a list of characters that if found at the start of a whisper, fires command received event. - /// - /// Character, that if found at start of message, fires command received event. - public void AddWhisperCommandIdentifier(char identifier) + /// + public async Task ReconnectAsync() { - if (!IsInitialized) HandleNotInitialized(); - _whisperCommandIdentifiers.Add(identifier); - } + if (!IsInitialized) + HandleNotInitialized(); - /// - /// Removes a character to a list of characters that if found at the start of a whisper, fires command received event. - /// - /// Command identifier to removed from identifier list. - public void RemoveWhisperCommandIdentifier(char identifier) - { - if (!IsInitialized) HandleNotInitialized(); - _whisperCommandIdentifiers.Remove(identifier); + _logger?.LogReconnecting(); + await _client.ReconnectAsync(); } #endregion #region ConnectionCredentials - - /// - /// Sets the connection credentials. - /// - /// The credentials. + /// /// While the client is connected, you are unable to change the connection credentials. Please disconnect first and then change them. public void SetConnectionCredentials(ConnectionCredentials credentials) { @@ -799,99 +548,94 @@ public void SetConnectionCredentials(ConnectionCredentials credentials) #endregion #region Channel Calls - /// - /// Join the Twitch IRC chat of . - /// - /// The channel to join. - /// Override a join check. - public void JoinChannel(string channel, bool overrideCheck = false) + + /// + public Task JoinChannelAsync(string channel, bool overrideCheck = false) { - if (!IsInitialized) HandleNotInitialized(); - if (!IsConnected) HandleNotConnected(); + if (!IsInitialized) + HandleNotInitialized(); + + if (!IsConnected) + HandleNotConnected(); + // Check to see if client is already in channel - if (JoinedChannels.FirstOrDefault(x => x.Channel.ToLower() == channel && !overrideCheck) != null) - return; - if (channel[0] == '#') channel = channel.Substring(1); + if (JoinedChannels.Any(x => !overrideCheck && x.Channel.Equals(channel, StringComparison.OrdinalIgnoreCase))) + return Task.CompletedTask; + + if (channel[0] == '#') + channel = channel.Substring(1); + _joinChannelQueue.Enqueue(new JoinedChannel(channel)); - if (!_currentlyJoiningChannels) - QueueingJoinCheck(); + + return !_currentlyJoiningChannels ? QueueingJoinCheckAsync() : Task.CompletedTask; } - /// - /// Returns a JoinedChannel object using a passed string/>. - /// - /// String channel to search for. - /// JoinedChannel. + /// /// Must be connected to at least one channel. - public JoinedChannel GetJoinedChannel(string channel) + public JoinedChannel? GetJoinedChannel(string channel) { - if (!IsInitialized) HandleNotInitialized(); + if (!IsInitialized) + HandleNotInitialized(); + if (JoinedChannels.Count == 0) throw new BadStateException("Must be connected to at least one channel."); - if (channel[0] == '#') channel = channel.Substring(1); + + if (channel[0] == '#') + channel = channel.Substring(1); + return _joinedChannelManager.GetJoinedChannel(channel); } - /// - /// Leaves (PART) the Twitch IRC chat of . - /// - /// The channel to leave. - /// True is returned if the passed channel was found, false if channel not found. - public void LeaveChannel(string channel) + /// + public async Task LeaveChannelAsync(string channel) { - if (!IsInitialized) HandleNotInitialized(); + if (!IsInitialized) + HandleNotInitialized(); + // Channel MUST be lower case channel = channel.ToLower(); - if (channel[0] == '#') channel = channel.Substring(1); - Log($"Leaving channel: {channel}", level: LogLevel.Information); - JoinedChannel joinedChannel = _joinedChannelManager.GetJoinedChannel(channel); + if (channel[0] == '#') + channel = channel.Substring(1); + + _logger?.LogLeavingChannel(channel); + var joinedChannel = _joinedChannelManager.GetJoinedChannel(channel); + if (joinedChannel != null) - _client.Send(Rfc2812.Part($"#{channel}")); + { + await _client.SendAsync(Rfc2812.Part($"#{channel}")); + _joinedChannelManager.RemoveJoinedChannel(channel); + } } - /// - /// Leaves (PART) the Twitch IRC chat of . - /// - /// The JoinedChannel object to leave. - /// True is returned if the passed channel was found, false if channel not found. - public void LeaveChannel(JoinedChannel channel) + /// + public Task LeaveChannelAsync(JoinedChannel channel) { - if (!IsInitialized) HandleNotInitialized(); - LeaveChannel(channel.Channel); + if (!IsInitialized) + HandleNotInitialized(); + + return LeaveChannelAsync(channel.Channel); } #endregion - /// - /// This method allows firing the message parser with a custom irc string allowing for easy testing - /// - /// This should be a raw IRC message resembling one received from Twitch IRC. - public void OnReadLineTest(string rawIrc) + /// + public Task OnReadLineTestAsync(string rawIrc) { - if (!IsInitialized) HandleNotInitialized(); - HandleIrcMessage(_ircParser.ParseIrcMessage(rawIrc)); + if (!IsInitialized) + HandleNotInitialized(); + + return HandleIrcMessageAsync(IrcParser.ParseMessage(rawIrc)); } #region Client Events - - /// - /// Handles the OnWhisperThrottled event of the _client control. - /// - /// The source of the event. - /// The instance containing the event data. - private void _client_OnWhisperThrottled(object sender, OnWhisperThrottledEventArgs e) + private Task OnThrottled(object? sender, OnMessageThrottledArgs e) { - OnWhisperThrottled?.Invoke(sender, e); + return OnMessageThrottled.TryInvoke(sender, e); } - - /// - /// Handles the OnMessageThrottled event of the _client control. - /// - /// The source of the event. - /// The instance containing the event data. - private void _client_OnMessageThrottled(object sender, OnMessageThrottledEventArgs e) + + private Task ThrottlerOnError(object? sender, OnErrorEventArgs e) { - OnMessageThrottled?.Invoke(sender, e); + return OnError.TryInvoke(sender, e); } /// @@ -899,9 +643,9 @@ private void _client_OnMessageThrottled(object sender, OnMessageThrottledEventAr /// /// The source of the event. /// The instance containing the event data. - private void _client_OnFatality(object sender, OnFatalErrorEventArgs e) + private Task _client_OnFatality(object? sender, OnFatalErrorEventArgs e) { - OnConnectionError?.Invoke(this, new OnConnectionErrorArgs { BotUsername = TwitchUsername, Error = new ErrorEvent { Message = e.Reason } }); + return OnConnectionError.TryInvoke(this, new(TwitchUsername, new(e.Reason))); } /// @@ -909,42 +653,67 @@ private void _client_OnFatality(object sender, OnFatalErrorEventArgs e) /// /// The source of the event. /// The instance containing the event data. - private void _client_OnDisconnected(object sender, OnDisconnectedEventArgs e) + private Task _client_OnDisconnected(object? sender, OnDisconnectedEventArgs e) { - OnDisconnected?.Invoke(sender, e); + return OnDisconnected.TryInvoke(sender, new(TwitchUsername)); } /// /// Handles the OnReconnected event of the _client control. /// /// The source of the event. - /// The instance containing the event data. - private void _client_OnReconnected(object sender, OnReconnectedEventArgs e) + /// The instance containing the event data. + private async Task _client_OnReconnected(object? sender, Communication.Events.OnConnectedEventArgs e) { + await SendHandshake(); + foreach (var channel in _joinedChannelManager.GetJoinedChannels()) - if(!string.Equals(channel.Channel, TwitchUsername, StringComparison.CurrentCultureIgnoreCase)) - _joinChannelQueue.Enqueue(channel); + { + _joinChannelQueue.Enqueue(channel); + } + + if (_joinChannelQueue?.Count > 0) + { + await QueueingJoinCheckAsync(); + } + _joinedChannelManager.Clear(); - OnReconnected?.Invoke(sender, e); + await OnReconnected.TryInvoke(sender, new Events.OnConnectedEventArgs(TwitchUsername)); } + static readonly string[] NewLineSeparator = new[] + { + "\r\n" + }; // dont modify!!! + /// /// Handles the OnMessage event of the _client control. /// /// The source of the event. /// The instance containing the event data. - private void _client_OnMessage(object sender, OnMessageEventArgs e) + private async Task _client_OnMessage(object? sender, OnMessageEventArgs e) { - string[] stringSeparators = new[] { "\r\n" }; - string[] lines = e.Message.Split(stringSeparators, StringSplitOptions.None); - foreach (string line in lines) + var lines = e.Message.Split(NewLineSeparator, StringSplitOptions.None); + foreach (var line in lines) { if (line.Length <= 1) continue; - Log($"Received: {line}", level: LogLevel.Trace); - OnSendReceiveData?.Invoke(this, new OnSendReceiveDataArgs { Direction = Enums.SendReceiveDirection.Received, Data = line }); - HandleIrcMessage(_ircParser.ParseIrcMessage(line)); + _logger?.LogReceived(line); + + await OnSendReceiveData.TryInvoke(this, new(SendReceiveDirection.Received, line)); + IrcMessage ircMessage; + try + { + ircMessage = IrcParser.ParseMessage(line); + } + catch (Exception ex) + { + _logger?.LogParsingError(line, ex); + OnError?.Invoke(this, new(ex)); + continue; + } + await HandleIrcMessageAsync(ircMessage); } } @@ -953,25 +722,32 @@ private void _client_OnMessage(object sender, OnMessageEventArgs e) ///
/// The sender. /// The e. - private void _client_OnConnected(object sender, object e) + private async Task _client_OnConnectedAsync(object? sender, EventArgs e) { - _client.Send(Rfc2812.Pass(ConnectionCredentials.TwitchOAuth)); - _client.Send(Rfc2812.Nick(ConnectionCredentials.TwitchUsername)); - _client.Send(Rfc2812.User(ConnectionCredentials.TwitchUsername, 0, ConnectionCredentials.TwitchUsername)); + await SendHandshake(); - if (ConnectionCredentials.Capabilities.Membership) - _client.Send("CAP REQ twitch.tv/membership"); - if (ConnectionCredentials.Capabilities.Commands) - _client.Send("CAP REQ twitch.tv/commands"); - if (ConnectionCredentials.Capabilities.Tags) - _client.Send("CAP REQ twitch.tv/tags"); - - if(_joinChannelQueue != null && _joinChannelQueue.Count > 0) + if (_joinChannelQueue?.Count > 0) { - QueueingJoinCheck(); + await QueueingJoinCheckAsync(); } } + /// + /// Send the handshake for the connection. + /// + private async Task SendHandshake() + { + await _client!.SendAsync(Rfc2812.Pass(ConnectionCredentials!.TwitchOAuth)); + await _client.SendAsync(Rfc2812.Nick(ConnectionCredentials.TwitchUsername)); + await _client.SendAsync(Rfc2812.User(ConnectionCredentials.TwitchUsername, 0, ConnectionCredentials.TwitchUsername)); + + if (ConnectionCredentials.Capabilities.Membership) + await _client.SendAsync("CAP REQ twitch.tv/membership"); + if (ConnectionCredentials.Capabilities.Commands) + await _client.SendAsync("CAP REQ twitch.tv/commands"); + if (ConnectionCredentials.Capabilities.Tags) + await _client.SendAsync("CAP REQ twitch.tv/tags"); + } #endregion #region Joining Stuff @@ -979,21 +755,21 @@ private void _client_OnConnected(object sender, object e) /// /// Queueings the join check. /// - private void QueueingJoinCheck() + private async Task QueueingJoinCheckAsync() { if (_joinChannelQueue.Count > 0) { _currentlyJoiningChannels = true; - JoinedChannel channelToJoin = _joinChannelQueue.Dequeue(); - Log($"Joining channel: {channelToJoin.Channel}", level: LogLevel.Information); + var channelToJoin = _joinChannelQueue.Dequeue(); + _logger?.LogJoiningChannel(channelToJoin.Channel); // important we set channel to lower case when sending join message - _client.Send(Rfc2812.Join($"#{channelToJoin.Channel.ToLower()}")); + await _client!.SendAsync(Rfc2812.Join($"#{channelToJoin.Channel.ToLower()}")); _joinedChannelManager.AddJoinedChannel(new JoinedChannel(channelToJoin.Channel)); StartJoinedChannelTimer(channelToJoin.Channel); } else { - Log("Finished channel joining queue."); + _logger?.LogChannelJoiningFinished(); } } @@ -1007,8 +783,8 @@ private void StartJoinedChannelTimer(string channel) { _joinTimer = new System.Timers.Timer(1000); _joinTimer.Elapsed += JoinChannelTimeout; - _awaitingJoins = new List>(); } + // channel is ToLower()'d because ROOMSTATE (which is the event the client uses to remove // this channel from _awaitingJoins list) contains the username as always lowercase. This means // if we don't ToLower(), the channel never gets removed, and FailureToReceiveJoinConfirmation @@ -1023,26 +799,26 @@ private void StartJoinedChannelTimer(string channel) /// /// The sender. /// The instance containing the event data. - private void JoinChannelTimeout(object sender, System.Timers.ElapsedEventArgs e) + private void JoinChannelTimeout(object? sender, System.Timers.ElapsedEventArgs e) { if (_awaitingJoins.Any()) { - List> expiredChannels = _awaitingJoins.Where(x => (DateTime.Now - x.Value).TotalSeconds > 5).ToList(); + var expiredChannels = _awaitingJoins.Where(x => (DateTime.Now - x.Value).TotalSeconds > 5).ToList(); if (expiredChannels.Any()) { _awaitingJoins.RemoveAll(x => (DateTime.Now - x.Value).TotalSeconds > 5); - foreach (KeyValuePair expiredChannel in expiredChannels) + foreach (var expiredChannel in expiredChannels) { _joinedChannelManager.RemoveJoinedChannel(expiredChannel.Key.ToLowerInvariant()); - OnFailureToReceiveJoinConfirmation?.Invoke(this, new OnFailureToReceiveJoinConfirmationArgs { Exception = new FailureToReceiveJoinConfirmationException(expiredChannel.Key) }); + _ = OnFailureToReceiveJoinConfirmation?.TryInvoke(this, new(new(expiredChannel.Key))); } } } else { - _joinTimer.Stop(); + _joinTimer!.Stop(); _currentlyJoiningChannels = false; - QueueingJoinCheck(); + QueueingJoinCheckAsync().GetAwaiter().GetResult(); } } @@ -1054,90 +830,44 @@ private void JoinChannelTimeout(object sender, System.Timers.ElapsedEventArgs e) /// Handles the irc message. /// /// The irc message. - private void HandleIrcMessage(IrcMessage ircMessage) + private Task HandleIrcMessageAsync(IrcMessage ircMessage) { - if (ircMessage.ToString().StartsWith(":tmi.twitch.tv NOTICE * :Login authentication failed")) + var rawMessage = ircMessage.ToString(); + if (rawMessage.StartsWith(":tmi.twitch.tv NOTICE * :Login authentication failed")) { - OnIncorrectLogin?.Invoke(this, new OnIncorrectLoginArgs { Exception = new ErrorLoggingInException(ircMessage.ToString(), TwitchUsername) }); - return; + return OnIncorrectLogin.TryInvoke(this, new(new(rawMessage, TwitchUsername))); } - switch (ircMessage.Command) + + return ircMessage.Command switch { - case IrcCommand.PrivMsg: - HandlePrivMsg(ircMessage); - return; - case IrcCommand.Notice: - HandleNotice(ircMessage); - break; - case IrcCommand.Ping: - if (!DisableAutoPong) - SendRaw("PONG"); - return; - case IrcCommand.Pong: - return; - case IrcCommand.Join: - HandleJoin(ircMessage); - break; - case IrcCommand.Part: - HandlePart(ircMessage); - break; - case IrcCommand.ClearChat: - HandleClearChat(ircMessage); - break; - case IrcCommand.ClearMsg: - HandleClearMsg(ircMessage); - break; - case IrcCommand.UserState: - HandleUserState(ircMessage); - break; - case IrcCommand.GlobalUserState: - break; - case IrcCommand.RPL_001: - break; - case IrcCommand.RPL_002: - break; - case IrcCommand.RPL_003: - break; - case IrcCommand.RPL_004: - Handle004(); - break; - case IrcCommand.RPL_353: - Handle353(ircMessage); - break; - case IrcCommand.RPL_366: - Handle366(); - break; - case IrcCommand.RPL_372: - break; - case IrcCommand.RPL_375: - break; - case IrcCommand.RPL_376: - break; - case IrcCommand.Whisper: - HandleWhisper(ircMessage); - break; - case IrcCommand.RoomState: - HandleRoomState(ircMessage); - break; - case IrcCommand.Reconnect: - Reconnect(); - break; - case IrcCommand.UserNotice: - HandleUserNotice(ircMessage); - break; - case IrcCommand.Mode: - HandleMode(ircMessage); - break; - case IrcCommand.Cap: - HandleCap(ircMessage); - break; - case IrcCommand.Unknown: - // fall through - default: - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = null, Location = "HandleIrcMessage", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); - break; - } + IrcCommand.PrivMsg => HandlePrivMsg(ircMessage), + IrcCommand.Join => HandleJoin(ircMessage), + IrcCommand.Part => HandlePart(ircMessage), + IrcCommand.Ping => !DisableAutoPong ? SendRawAsync($"PONG {ircMessage.ToString().Substring(5)}") : Task.CompletedTask, + IrcCommand.Notice => HandleNotice(ircMessage), + IrcCommand.Whisper => HandleWhisper(ircMessage), + IrcCommand.ClearChat => HandleClearChat(ircMessage), + IrcCommand.ClearMsg => HandleClearMsg(ircMessage), + IrcCommand.UserState => HandleUserState(ircMessage), + IrcCommand.UserNotice => HandleUserNotice(ircMessage), + IrcCommand.RoomState => HandleRoomState(ircMessage), + IrcCommand.Reconnect => ReconnectAsync(), + IrcCommand.Cap => HandleCap(ircMessage), + IrcCommand.RPL_004 => Handle004(), + IrcCommand.RPL_353 => Handle353(ircMessage), + IrcCommand.RPL_366 => Handle366(), + + IrcCommand.Pong or + IrcCommand.RPL_001 or + IrcCommand.RPL_002 or + IrcCommand.RPL_003 or + IrcCommand.RPL_372 or + IrcCommand.RPL_375 or + IrcCommand.RPL_376 or + IrcCommand.GlobalUserState => Task.CompletedTask, + + IrcCommand.Unknown or _ => OnUnaccountedFor?.Invoke(this, new(TwitchUsername, null, "HandleIrcMessage", rawMessage)) ?? UnaccountedFor(rawMessage) + }; } #region IrcCommand Handling @@ -1146,151 +876,141 @@ private void HandleIrcMessage(IrcMessage ircMessage) /// Handles the priv MSG. /// /// The irc message. - private void HandlePrivMsg(IrcMessage ircMessage) + private async Task HandlePrivMsg(IrcMessage ircMessage) { - ChatMessage chatMessage = new ChatMessage(TwitchUsername, ircMessage, ref _channelEmotes, WillReplaceEmotes); - foreach (JoinedChannel joinedChannel in JoinedChannels.Where(x => string.Equals(x.Channel, ircMessage.Channel, StringComparison.InvariantCultureIgnoreCase))) + var chatMessage = new ChatMessage( + TwitchUsername, + ircMessage, + ChannelEmotes, + WillReplaceEmotes, + ReplacedEmotesPrefix, + ReplacedEmotesSuffix); + + foreach (JoinedChannel joinedChannel in JoinedChannels + .Where(x => x.Channel.Equals(ircMessage.Channel, StringComparison.InvariantCultureIgnoreCase))) + { joinedChannel.HandleMessage(chatMessage); + } - OnMessageReceived?.Invoke(this, new OnMessageReceivedArgs { ChatMessage = chatMessage }); + await OnMessageReceived.TryInvoke(this, new(chatMessage)); - if (ircMessage.Tags.TryGetValue(Tags.MsgId, out var msgId)) - if (msgId == MsgIds.UserIntro) - OnUserIntro?.Invoke(this, new OnUserIntroArgs { ChatMessage = chatMessage }); + if (ircMessage.Tags.TryGetValue(Tags.MsgId, out var msgId) + && msgId == MsgIds.UserIntro + && OnUserIntro != null) + { + await OnUserIntro.Invoke(this, new(chatMessage)); + } - if (_chatCommandIdentifiers != null && _chatCommandIdentifiers.Count != 0 && !string.IsNullOrEmpty(chatMessage.Message)) + if (OnChatCommandReceived is not null + && CanInvokeCommand(ChatCommandIdentifiers, chatMessage.Message.AsSpan(), out var commandInfo)) { - if (_chatCommandIdentifiers.Contains(chatMessage.Message[0])) - { - ChatCommand chatCommand = new ChatCommand(chatMessage); - OnChatCommandReceived?.Invoke(this, new OnChatCommandReceivedArgs { Command = chatCommand }); - return; - } + await OnChatCommandReceived.Invoke(this, new(chatMessage, commandInfo!)); } + } /// /// Handles the notice. /// /// The irc message. - private void HandleNotice(IrcMessage ircMessage) + private Task HandleNotice(IrcMessage ircMessage) { - if (ircMessage.Message.Contains("Improperly formatted auth")) - { - OnIncorrectLogin?.Invoke(this, new OnIncorrectLoginArgs { Exception = new ErrorLoggingInException(ircMessage.ToString(), TwitchUsername) }); - return; - } + var channel = ircMessage.Channel; + var message = ircMessage.Message; + var rawIrcMessage = ircMessage.ToString(); - bool success = ircMessage.Tags.TryGetValue(Tags.MsgId, out string msgId); - if (!success) + // This check might be too fragile and catch false positives + if (message.Contains("Improperly formatted auth")) { - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel, Location = "NoticeHandling", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); + return OnIncorrectLogin.TryInvoke(this, new(new(rawIrcMessage, TwitchUsername))); } - switch (msgId) + var success = ircMessage.Tags.TryGetValue(Tags.MsgId, out string? msgId); + //if (!success) + //{ + // return OnUnaccountedFor?.Invoke(this, new() + // { + // BotUsername = TwitchUsername, + // Channel = channel, + // Location = "NoticeHandling", + // RawIRC = rawIrcMessage + // }) ?? UnaccountedFor(rawIrcMessage); + //} + + var result = msgId switch { - case MsgIds.ColorChanged: - OnChatColorChanged?.Invoke(this, new OnChatColorChangedArgs { Channel = ircMessage.Channel }); - break; - case MsgIds.ModeratorsReceived: - OnModeratorsReceived?.Invoke(this, new OnModeratorsReceivedArgs { Channel = ircMessage.Channel, Moderators = ircMessage.Message.Replace(" ", "").Split(':')[1].Split(',').ToList() }); - break; - case MsgIds.NoMods: - OnModeratorsReceived?.Invoke(this, new OnModeratorsReceivedArgs { Channel = ircMessage.Channel, Moderators = new List() }); - break; - case MsgIds.NoPermission: - OnNoPermissionError?.Invoke(this, null); - break; - case MsgIds.RaidErrorSelf: - OnSelfRaidError?.Invoke(this, null); - break; - case MsgIds.RaidNoticeMature: - OnRaidedChannelIsMatureAudience?.Invoke(this, null); - break; - case MsgIds.MsgBannedEmailAlias: - OnBannedEmailAlias?.Invoke(this, new OnBannedEmailAliasArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgChannelSuspended: - _awaitingJoins.RemoveAll(x => x.Key.ToLower() == ircMessage.Channel); - _joinedChannelManager.RemoveJoinedChannel(ircMessage.Channel); - QueueingJoinCheck(); - OnFailureToReceiveJoinConfirmation?.Invoke(this, new OnFailureToReceiveJoinConfirmationArgs - { - Exception = new FailureToReceiveJoinConfirmationException(ircMessage.Channel, ircMessage.Message) - }); - break; - case MsgIds.MsgRequiresVerifiedPhoneNumber: - OnRequiresVerifiedPhoneNumber?.Invoke(this, new OnRequiresVerifiedPhoneNumberArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgVerifiedEmail: - OnRequiresVerifiedEmail?.Invoke(this, new OnRequiresVerifiedEmailArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.NoVIPs: - OnVIPsReceived?.Invoke(this, new OnVIPsReceivedArgs { Channel = ircMessage.Channel, VIPs = new List() }); - break; - case MsgIds.VIPsSuccess: - OnVIPsReceived?.Invoke(this, new OnVIPsReceivedArgs { Channel = ircMessage.Channel, VIPs = ircMessage.Message.Replace(" ", "").Replace(".", "").Split(':')[1].Split(',').ToList() }); - break; - case MsgIds.MsgRateLimit: - OnRateLimit?.Invoke(this, new OnRateLimitArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgDuplicate: - OnDuplicate?.Invoke(this, new OnDuplicateArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgFollowersOnly: - OnFollowersOnly?.Invoke(this, new OnFollowersOnlyArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgSubsOnly: - OnSubsOnly?.Invoke(this, new OnSubsOnlyArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgEmoteOnly: - OnEmoteOnly?.Invoke(this, new OnEmoteOnlyArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgSuspended: - OnSuspended?.Invoke(this, new OnSuspendedArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgBanned: - OnBanned?.Invoke(this, new OnBannedArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgSlowMode: - OnSlowMode?.Invoke(this, new OnSlowModeArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - case MsgIds.MsgR9k: - OnR9kMode?.Invoke(this, new OnR9kModeArgs { Channel = ircMessage.Channel, Message = ircMessage.Message }); - break; - - default: - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel, Location = "NoticeHandling", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); - break; - } + MsgIds.NoPermission => OnNoPermissionError?.Invoke(this, new(channel, message)), + MsgIds.RaidErrorSelf => OnSelfRaidError?.Invoke(this, new(channel, message)), + MsgIds.RaidNoticeMature => OnRaidedChannelIsMatureAudience?.Invoke(this, new(channel, message)), + MsgIds.MsgBannedEmailAlias => OnBannedEmailAlias?.Invoke(this, new(channel, message)), + MsgIds.MsgChannelSuspended => HandleChannelSuspended(ircMessage), + MsgIds.MsgRequiresVerifiedPhoneNumber => OnRequiresVerifiedPhoneNumber?.Invoke(this, new(channel, message)), + MsgIds.MsgVerifiedEmail => OnRequiresVerifiedEmail?.Invoke(this, new(channel, message)), + MsgIds.MsgRateLimit => OnRateLimit?.Invoke(this, new(channel, message)), + MsgIds.MsgDuplicate => OnDuplicate?.Invoke(this, new(channel, message)), + MsgIds.MsgFollowersOnly => OnFollowersOnly?.Invoke(this, new(channel, message)), + MsgIds.MsgSubsOnly => OnSubsOnly?.Invoke(this, new(channel, message)), + MsgIds.MsgEmoteOnly => OnEmoteOnly?.Invoke(this, new(channel, message)), + MsgIds.MsgSuspended => OnSuspended?.Invoke(this, new(channel, message)), + MsgIds.MsgBanned => OnBanned?.Invoke(this, new(channel, message)), + MsgIds.MsgSlowMode => OnSlowMode?.Invoke(this, new(channel, message)), + MsgIds.SlowOn => OnSlowModeChannel?.Invoke(this, new(channel, message, true)), + MsgIds.SlowOff => OnSlowModeChannel?.Invoke(this, new(channel, message, false)), + MsgIds.MsgR9k => OnR9kMode?.Invoke(this, new(channel, message)), + _ => OnUnaccountedFor?.Invoke(this, new(TwitchUsername, channel, "NoticeHandling", rawIrcMessage)) ?? UnaccountedFor(rawIrcMessage) + }; + return result ?? Task.CompletedTask; + } + + /// + /// Handles the channel suspended message + /// + /// The irc message. + private async Task HandleChannelSuspended(IrcMessage ircMessage) + { + _awaitingJoins.RemoveAll(x => x.Key.Equals(ircMessage.Channel, StringComparison.OrdinalIgnoreCase)); + _joinedChannelManager.RemoveJoinedChannel(ircMessage.Channel); + + await QueueingJoinCheckAsync(); + await OnFailureToReceiveJoinConfirmation.TryInvoke(this, new( + new(ircMessage.Channel, ircMessage.Message))); } /// /// Handles the join. /// /// The irc message. - private void HandleJoin(IrcMessage ircMessage) + private Task HandleJoin(IrcMessage ircMessage) { - OnUserJoined?.Invoke(this, new OnUserJoinedArgs { Channel = ircMessage.Channel, Username = ircMessage.User }); + if (string.Equals(TwitchUsername, ircMessage.User, StringComparison.InvariantCultureIgnoreCase)) + { + var channel = _awaitingJoins.Find(x => x.Key == ircMessage.Channel); + _awaitingJoins.Remove(channel); + + return OnJoinedChannel.TryInvoke(this, new(ircMessage.Channel, TwitchUsername)); + } + else + { + return OnUserJoined.TryInvoke(this, new(ircMessage.Channel, ircMessage.User)); + } } /// /// Handles the part. /// /// The irc message. - private void HandlePart(IrcMessage ircMessage) + private Task HandlePart(IrcMessage ircMessage) { if (string.Equals(TwitchUsername, ircMessage.User, StringComparison.InvariantCultureIgnoreCase)) { _joinedChannelManager.RemoveJoinedChannel(ircMessage.Channel); _hasSeenJoinedChannels.Remove(ircMessage.Channel); - OnLeftChannel?.Invoke(this, new OnLeftChannelArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel }); + + return OnLeftChannel.TryInvoke(this,new(ircMessage.Channel, TwitchUsername)); } else { - OnUserLeft?.Invoke(this, new OnUserLeftArgs { Channel = ircMessage.Channel, Username = ircMessage.User }); + return OnUserLeft.TryInvoke(this, new(ircMessage.Channel, ircMessage.User)); } } @@ -1298,271 +1018,198 @@ private void HandlePart(IrcMessage ircMessage) /// Handles the clear chat. /// /// The irc message. - private void HandleClearChat(IrcMessage ircMessage) + private Task HandleClearChat(IrcMessage ircMessage) { if (string.IsNullOrWhiteSpace(ircMessage.Message)) { - OnChatCleared?.Invoke(this, new OnChatClearedArgs { Channel = ircMessage.Channel }); - return; + return OnChatCleared.TryInvoke(this, new(ircMessage.Channel)); } - bool successBanDuration = ircMessage.Tags.TryGetValue(Tags.BanDuration, out _); - if (successBanDuration) - { - UserTimeout userTimeout = new UserTimeout(ircMessage); - OnUserTimedout?.Invoke(this, new OnUserTimedoutArgs { UserTimeout = userTimeout }); - return; - } + var successBanDuration = ircMessage.Tags.ContainsKey(Tags.BanDuration); - UserBan userBan = new UserBan(ircMessage); - OnUserBanned?.Invoke(this, new OnUserBannedArgs { UserBan = userBan }); + return successBanDuration + ? OnUserTimedout.TryInvoke(this, new(new(ircMessage))) + : OnUserBanned.TryInvoke(this, new(new(ircMessage))); } /// /// Handles the clear MSG. /// /// The irc message. - private void HandleClearMsg(IrcMessage ircMessage) + private Task HandleClearMsg(IrcMessage ircMessage) { - OnMessageCleared?.Invoke(this, new OnMessageClearedArgs { Channel = ircMessage.Channel, Message = ircMessage.Message, TargetMessageId = ircMessage.ToString().Split('=')[3].Split(';')[0], TmiSentTs = ircMessage.ToString().Split('=')[4].Split(' ')[0] }); + var tmiSent = ircMessage.Tags.TryGetValue("tmi-sent-ts", out var tmiSentTs) + ? DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(tmiSentTs)) + : default; + return OnMessageCleared.TryInvoke(this, new( + ircMessage.Channel, + ircMessage.Message, + ircMessage.Tags.GetValueOrDefault("target-msg-id", string.Empty), + tmiSent + )); } /// /// Handles the state of the user. /// /// The irc message. - private void HandleUserState(IrcMessage ircMessage) + private Task HandleUserState(IrcMessage ircMessage) { - UserState userState = new UserState(ircMessage); - if (!_hasSeenJoinedChannels.Contains(userState.Channel.ToLowerInvariant())) + var userState = new UserState(ircMessage); + var userChannel = userState.Channel.ToLowerInvariant(); + if (!_hasSeenJoinedChannels.Contains(userChannel)) { _hasSeenJoinedChannels.Add(userState.Channel.ToLowerInvariant()); - OnUserStateChanged?.Invoke(this, new OnUserStateChangedArgs { UserState = userState }); + return OnUserStateChanged.TryInvoke(this, new(userState)); } - else - OnMessageSent?.Invoke(this, new OnMessageSentArgs { SentMessage = new SentMessage(userState, _lastMessageSent) }); + + return OnMessageSent.TryInvoke(this, new(new(userState, _lastMessageSent))); } /// /// Handle004s this instance. /// - private void Handle004() + private Task Handle004() { - OnConnected?.Invoke(this, new OnConnectedArgs { BotUsername = TwitchUsername }); + return OnConnected.TryInvoke(this, new(TwitchUsername)); } /// /// Handle353s the specified irc message. /// /// The irc message. - private void Handle353(IrcMessage ircMessage) + private Task Handle353(IrcMessage ircMessage) { - OnExistingUsersDetected?.Invoke(this, new OnExistingUsersDetectedArgs { Channel = ircMessage.Channel, Users = ircMessage.Message.Split(' ').ToList() }); + return OnExistingUsersDetected.TryInvoke(this, new(ircMessage.Channel, ircMessage.Message.Split(' ').ToList())); } /// /// Handle366s this instance. /// - private void Handle366() + private Task Handle366() { _currentlyJoiningChannels = false; - QueueingJoinCheck(); + return QueueingJoinCheckAsync(); } /// /// Handles the whisper. /// /// The irc message. - private void HandleWhisper(IrcMessage ircMessage) + private async Task HandleWhisper(IrcMessage ircMessage) { - WhisperMessage whisperMessage = new WhisperMessage(ircMessage, TwitchUsername); + var whisperMessage = new WhisperMessage(ircMessage, TwitchUsername); PreviousWhisper = whisperMessage; - OnWhisperReceived?.Invoke(this, new OnWhisperReceivedArgs { WhisperMessage = whisperMessage }); - if (_whisperCommandIdentifiers != null && _whisperCommandIdentifiers.Count != 0 && !string.IsNullOrEmpty(whisperMessage.Message)) - if (_whisperCommandIdentifiers.Contains(whisperMessage.Message[0])) - { - WhisperCommand whisperCommand = new WhisperCommand(whisperMessage); - OnWhisperCommandReceived?.Invoke(this, new OnWhisperCommandReceivedArgs { Command = whisperCommand }); - return; - } - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel, Location = "WhispergHandling", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); + await OnWhisperReceived.TryInvoke(this, new(whisperMessage)); + + if (OnWhisperCommandReceived is not null + && CanInvokeCommand(WhisperCommandIdentifiers, whisperMessage.Message.AsSpan(), out var commandInfo)) + { + await OnWhisperCommandReceived.Invoke(this, new(whisperMessage, commandInfo!)); + } } - /// - /// Handles the state of the room. - /// - /// The irc message. - private void HandleRoomState(IrcMessage ircMessage) + static bool CanInvokeCommand(ICollection commandIdentifiers, ReadOnlySpan message, out CommandInfo? commandInfo) { - // If ROOMSTATE is sent because a mode (subonly/slow/emote/etc) is being toggled, it has two tags: room-id, and the specific mode being toggled - // If ROOMSTATE is sent because of a join confirmation, all tags (ie greater than 2) are sent - if (ircMessage.Tags.Count > 2) + commandInfo = null; + if (message.IsEmpty) + return false; + + foreach (var commandIdentifier in commandIdentifiers) { - KeyValuePair channel = _awaitingJoins.FirstOrDefault(x => x.Key == ircMessage.Channel); - _awaitingJoins.Remove(channel); - OnJoinedChannel?.Invoke(this, new OnJoinedChannelArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel }); + if (message.StartsWith(commandIdentifier.AsSpan())) + { + return CommandInfo.TryParse(commandIdentifier, message, out commandInfo); + } } - - OnChannelStateChanged?.Invoke(this, new OnChannelStateChangedArgs { ChannelState = new ChannelState(ircMessage), Channel = ircMessage.Channel }); + return false; } /// - /// Handles the user notice. + /// Handles the state of the room. /// /// The irc message. - private void HandleUserNotice(IrcMessage ircMessage) + private Task HandleRoomState(IrcMessage ircMessage) { - bool successMsgId = ircMessage.Tags.TryGetValue(Tags.MsgId, out string msgId); - if (!successMsgId) - { - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel, Location = "UserNoticeHandling", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); - return; - } - - switch (msgId) - { - case MsgIds.Announcement: - Announcement announcement = new Announcement(ircMessage); - OnAnnouncement?.Invoke(this, new OnAnnouncementArgs { Announcement = announcement, Channel = ircMessage.Channel }); - break; - case MsgIds.Raid: - RaidNotification raidNotification = new RaidNotification(ircMessage); - OnRaidNotification?.Invoke(this, new OnRaidNotificationArgs { Channel = ircMessage.Channel, RaidNotification = raidNotification }); - break; - case MsgIds.ReSubscription: - ReSubscriber resubscriber = new ReSubscriber(ircMessage); - OnReSubscriber?.Invoke(this, new OnReSubscriberArgs { ReSubscriber = resubscriber, Channel = ircMessage.Channel }); - break; - case MsgIds.SubGift: - GiftedSubscription giftedSubscription = new GiftedSubscription(ircMessage); - OnGiftedSubscription?.Invoke(this, new OnGiftedSubscriptionArgs { GiftedSubscription = giftedSubscription, Channel = ircMessage.Channel }); - break; - case MsgIds.CommunitySubscription: - CommunitySubscription communitySubscription = new CommunitySubscription(ircMessage); - OnCommunitySubscription?.Invoke(this, new OnCommunitySubscriptionArgs { GiftedSubscription = communitySubscription, Channel = ircMessage.Channel }); - break; - case MsgIds.ContinuedGiftedSubscription: - ContinuedGiftedSubscription continuedGiftedSubscription = new ContinuedGiftedSubscription(ircMessage); - OnContinuedGiftedSubscription?.Invoke(this, new OnContinuedGiftedSubscriptionArgs { ContinuedGiftedSubscription = continuedGiftedSubscription, Channel = ircMessage.Channel }); - break; - case MsgIds.Subscription: - Subscriber subscriber = new Subscriber(ircMessage); - OnNewSubscriber?.Invoke(this, new OnNewSubscriberArgs { Subscriber = subscriber, Channel = ircMessage.Channel }); - break; - case MsgIds.PrimePaidUprade: - PrimePaidSubscriber primePaidSubscriber = new PrimePaidSubscriber(ircMessage); - OnPrimePaidSubscriber?.Invoke(this, new OnPrimePaidSubscriberArgs { PrimePaidSubscriber = primePaidSubscriber, Channel = ircMessage.Channel }); - break; - default: - OnUnaccountedFor?.Invoke(this, new OnUnaccountedForArgs { BotUsername = TwitchUsername, Channel = ircMessage.Channel, Location = "UserNoticeHandling", RawIRC = ircMessage.ToString() }); - UnaccountedFor(ircMessage.ToString()); - break; - } + return OnChannelStateChanged.TryInvoke(this, new(ircMessage.Channel, new ChannelState(ircMessage))); } /// - /// Handles the mode. + /// Handles the user notice. /// /// The irc message. - private void HandleMode(IrcMessage ircMessage) + private Task HandleUserNotice(IrcMessage ircMessage) { - if (ircMessage.Message.StartsWith("+o")) - { - OnModeratorJoined?.Invoke(this, new OnModeratorJoinedArgs { Channel = ircMessage.Channel, Username = ircMessage.Message.Split(' ')[1] }); - return; - } - - if (ircMessage.Message.StartsWith("-o")) + var rawMessage = ircMessage.ToString(); + + ircMessage.Tags.TryGetValue(Tags.MsgId, out string? msgId); + //if(!ircMessage.Tags.TryGetValue(Tags.MsgId, out string? msgId)) + //{ + // return OnUnaccountedFor?.Invoke(this, new() + // { + // BotUsername = TwitchUsername, + // Channel = ircMessage.Channel, + // Location = "UserNoticeHandling", + // RawIRC = rawMessage + // }) ?? UnaccountedFor(rawMessage); + //} + + return msgId switch { - OnModeratorLeft?.Invoke(this, new OnModeratorLeftArgs { Channel = ircMessage.Channel, Username = ircMessage.Message.Split(' ')[1] }); - } + MsgIds.Announcement => OnAnnouncement.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.Raid => OnRaidNotification.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.ReSubscription => OnReSubscriber.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.SubGift => OnGiftedSubscription.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.CommunitySubscription => OnCommunitySubscription.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.ContinuedGiftedSubscription => OnContinuedGiftedSubscription.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.Subscription => OnNewSubscriber.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.PrimePaidUprade => OnPrimePaidSubscriber.TryInvoke(this, new(ircMessage.Channel, new(ircMessage))), + MsgIds.AnonGiftPaidUpgrade => OnAnonGiftPaidUpgrade.TryInvoke(this, new(ircMessage)), + MsgIds.Unraid => OnUnraidNotification.TryInvoke(this, new(ircMessage)), + MsgIds.Ritual => OnRitual.TryInvoke(this, new(ircMessage)), + MsgIds.BitsBadgeTier => OnBitsBadgeTier.TryInvoke(this, new(ircMessage)), + MsgIds.CommunityPayForward => OnCommunityPayForward.TryInvoke(this, new(ircMessage)), + MsgIds.StandardPayForward => OnStandardPayForward.TryInvoke(this, new(ircMessage)), + _ => OnUnaccountedFor?.Invoke(this, new(TwitchUsername, ircMessage.Channel, "UserNoticeHandling", rawMessage)) ?? UnaccountedFor(rawMessage) + }; } /// /// Handles the Cap /// - /// The irc message - private void HandleCap(IrcMessage ircMessage) + private static Task HandleCap(IrcMessage _) { - // do nothing - return; + // do nothing, actually cap frfr + return Task.CompletedTask; } - #endregion +#endregion - #endregion +#endregion - private void UnaccountedFor(string ircString) + private Task UnaccountedFor(string ircString) { - Log($"Unaccounted for: {ircString} (please create a TwitchLib GitHub issue :P)", level: LogLevel.Warning); + _logger?.LogUnaccountedFor(ircString); + return Task.CompletedTask; } - /// - /// Logs the specified message. - /// - /// The message. - /// if set to true [include date]. - /// if set to true [include time]. - /// The log level of the message. - private void Log(string message, bool includeDate = false, bool includeTime = false, LogLevel level = LogLevel.Debug) + /// + public Task SendQueuedItemAsync(string message) { - string dateTimeStr; - if (includeDate && includeTime) - dateTimeStr = $"{DateTime.UtcNow}"; - else if (includeDate) - dateTimeStr = $"{DateTime.UtcNow.ToShortDateString()}"; - else - dateTimeStr = $"{DateTime.UtcNow.ToShortTimeString()}"; - - if (includeDate || includeTime) - _logger?.Log(level, $"[TwitchLib, {Assembly.GetExecutingAssembly().GetName().Version} - {dateTimeStr}] {message}"); - else - _logger?.Log(level, $"[TwitchLib, {Assembly.GetExecutingAssembly().GetName().Version}] {message}"); - - OnLog?.Invoke(this, new OnLogArgs { BotUsername = ConnectionCredentials?.TwitchUsername, Data = message, DateTime = DateTime.UtcNow }); - } - - /// - /// Logs the error. - /// - /// The message. - /// if set to true [include date]. - /// if set to true [include time]. - private void LogError(string message, bool includeDate = false, bool includeTime = false) - { - string dateTimeStr; - if (includeDate && includeTime) - dateTimeStr = $"{DateTime.UtcNow}"; - else if (includeDate) - dateTimeStr = $"{DateTime.UtcNow.ToShortDateString()}"; - else - dateTimeStr = $"{DateTime.UtcNow.ToShortTimeString()}"; - - if (includeDate || includeTime) - _logger?.LogError($"[TwitchLib, {Assembly.GetExecutingAssembly().GetName().Version} - {dateTimeStr}] {message}"); - else - _logger?.LogError($"[TwitchLib, {Assembly.GetExecutingAssembly().GetName().Version}] {message}"); - - OnLog?.Invoke(this, new OnLogArgs { BotUsername = ConnectionCredentials?.TwitchUsername, Data = message, DateTime = DateTime.UtcNow }); - } + if (!IsInitialized) + HandleNotInitialized(); - /// - /// Sends the queued item. - /// - /// The message. - public void SendQueuedItem(string message) - { - if (!IsInitialized) HandleNotInitialized(); - _client.Send(message); + return _client.SendAsync(message); } /// /// Handles the not initialized. /// /// The twitch client has not been initialized and cannot be used. Please call Initialize(); +#if NETSTANDARD2_1 || NET + [DoesNotReturn] +#endif protected static void HandleNotInitialized() { throw new ClientNotInitializedException("The twitch client has not been initialized and cannot be used. Please call Initialize();"); @@ -1572,6 +1219,9 @@ protected static void HandleNotInitialized() /// Handles the not connected. /// /// In order to perform this action, the client must be connected to Twitch. To confirm connection, try performing this action in or after the OnConnected event has been fired. +#if NETSTANDARD2_1 || NET + [DoesNotReturn] +#endif protected static void HandleNotConnected() { throw new ClientNotConnectedException("In order to perform this action, the client must be connected to Twitch. To confirm connection, try performing this action in or after the OnConnected event has been fired."); diff --git a/TwitchLib.Client/TwitchLib.Client.csproj b/TwitchLib.Client/TwitchLib.Client.csproj index 7ff111b0..e10a8131 100644 --- a/TwitchLib.Client/TwitchLib.Client.csproj +++ b/TwitchLib.Client/TwitchLib.Client.csproj @@ -1,33 +1,29 @@  - netstandard2.0 - TwitchLib.Client - 3.4.0 - $(VersionSuffix) + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 Client component of TwitchLib. This component allows you to access Twitch chat and whispers, as well as the events that are sent over this connection. - true - swiftyspiffy, Prom3theu5, Syzuna, LuckyNoS7evin - https://cdn.syzuna-programs.de/images/twitchlib.png - https://github.com/TwitchLib/TwitchLib.Client - https://opensource.org/licenses/MIT - Copyright 2023 - - https://github.com/TwitchLib/TwitchLib.Client - Git - twitch library irc chat c# csharp api events pubsub net standard 2.0 - en-US - 3.4.0 - 3.4.0 - True - - + + + + + + + + <_Parameter1>TwitchLib.Client.Test + + + + <_Parameter1>TwitchLib.Client.Benchmark + + + diff --git a/TwitchLib.Communication b/TwitchLib.Communication deleted file mode 160000 index 5b98f263..00000000 --- a/TwitchLib.Communication +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5b98f263a0606b14ac84a36221ae3d1a01d9bcd3 diff --git a/twitchlib.png b/twitchlib.png new file mode 100644 index 00000000..25683ee8 Binary files /dev/null and b/twitchlib.png differ