diff --git a/TPP.Core/Chat/TwitchEventSubChat.cs b/TPP.Core/Chat/TwitchEventSubChat.cs index 78f48511..2e51ff9d 100644 --- a/TPP.Core/Chat/TwitchEventSubChat.cs +++ b/TPP.Core/Chat/TwitchEventSubChat.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -525,6 +526,22 @@ await _overlayConnection.Send(new NewSubscriber }, CancellationToken.None); } + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + /// Parsing the emotes of the message sadly is not trivial, see . + /// Of course, we currently don't even need the indices and could just look up an emote's code by its ID using the + /// Twitch API (maybe we don't even need the emote code in the webapp?), but this saves us roundtrips to Twitch. + public static ImmutableList ParseEmotes(ChannelSubscriptionMessage.Emote[] emotes, string text) + { + byte[] messageBytes = Utf8NoBom.GetBytes(text); + return emotes.Select(emote => + { + int endExclusive = emote.End + 1; + string emoteCode = Utf8NoBom.GetString(messageBytes[emote.Begin..endExclusive]); + return new EmoteOccurrence(emote.Id, emoteCode); + }).ToImmutableList(); + } + private async Task ChannelSubscriptionMessageReceived(ChannelSubscriptionMessage channelSubscriptionMessage) { ChannelSubscriptionMessage.Event evt = channelSubscriptionMessage.Payload.Event; @@ -544,9 +561,9 @@ private async Task ChannelSubscriptionMessageReceived(ChannelSubscriptionMessage SubscriptionAt: channelSubscriptionMessage.Metadata.MessageTimestamp, IsGift: false, // Resubscriptions are never gifts. Gifts are always "new" subscriptions, not continuations Message: evt.Message?.Text, - Emotes: (evt.Message?.Emotes ?? []).Select(e => new EmoteOccurrence( - e.Id, evt.Message!.Text!.Substring(e.Begin, e.End - e.Begin), e.Begin, e.End)) - .ToImmutableList() + Emotes: evt.Message?.Emotes is { Length: > 0 } + ? ParseEmotes(evt.Message.Emotes, evt.Message.Text!) + : [] ); ISubscriptionProcessor.SubResult subResult = await _subscriptionProcessor.ProcessSubscription( subscriptionInfo); diff --git a/TPP.Core/Subscriptions.cs b/TPP.Core/Subscriptions.cs index 86cb50a8..9f24df0a 100644 --- a/TPP.Core/Subscriptions.cs +++ b/TPP.Core/Subscriptions.cs @@ -9,10 +9,13 @@ using TPP.Common; using TPP.Model; using TPP.Persistence; +using TPP.Twitch.EventSub.Notifications; namespace TPP.Core { - public record EmoteOccurrence(string Id, string Code, int StartIndex, int EndIndex); + /// This used to also include int StartIndex and int EndIndex, but since those were unused and how + /// to count was ambiguous (see ), they were removed. + public record EmoteOccurrence(string Id, string Code); /// /// Information on a user subscription directly (not via a gift). diff --git a/TPP.Twitch.EventSub/Notifications/ChannelSubscriptionMessage.cs b/TPP.Twitch.EventSub/Notifications/ChannelSubscriptionMessage.cs index b531bb69..d090fa15 100644 --- a/TPP.Twitch.EventSub/Notifications/ChannelSubscriptionMessage.cs +++ b/TPP.Twitch.EventSub/Notifications/ChannelSubscriptionMessage.cs @@ -27,6 +27,19 @@ public record Condition(string BroadcasterUserId) : EventSub.Condition; /// The index of where the Emote starts in the text. /// The index of where the Emote ends in the text. /// The emote ID. + /// Some additional gotchas that are not mentioned in the official documentation: + /// - The positions are counted in what appears to be UTF-8 bytes + /// - The begin is 0-based and inclusive (unsurprising), but the end is also inclusive (kinda surprising). + /// An experiment showed that this string: + /// test Kappa 🌿 BabyRage 🐍 PogChamp 🎅🏿 RaccAttack ♥ PraiseIt 12345678901234567890 + /// resulted in these emote indices: + /// + /// {"begin": 5, "end": 9, "id": "25"}, + /// {"begin": 16, "end": 23, "id": "22639"}, + /// {"begin": 30, "end": 37, "id": "305954156"}, + /// {"begin": 48, "end": 57, "id": "114870"}, + /// {"begin": 63, "end": 70, "id": "38586"} + /// public record Emote( int Begin, int End, diff --git a/tests/TPP.Core.Tests/Chat/TwitchEventSubChatTest.cs b/tests/TPP.Core.Tests/Chat/TwitchEventSubChatTest.cs new file mode 100644 index 00000000..996515f5 --- /dev/null +++ b/tests/TPP.Core.Tests/Chat/TwitchEventSubChatTest.cs @@ -0,0 +1,38 @@ +using System.Collections.Immutable; +using NUnit.Framework; +using TPP.Core.Chat; +using TPP.Twitch.EventSub.Notifications; + +namespace TPP.Core.Tests.Chat; + +[TestFixture] +[TestOf(typeof(TwitchEventSubChat))] +public class TwitchEventSubChatTest +{ + /// + /// The data comes from an actual subscription that was observed. + /// See also + /// + [Test] + public void ParseSubscriptionMessageEmotes() + { + const string message = "test Kappa 🌿 BabyRage 🐍 PogChamp 🎅🏿 RaccAttack ♥ PraiseIt 12345678901234567890"; + ChannelSubscriptionMessage.Emote[] emotes = + [ + new(5, 9, "25"), + new(16, 23, "22639"), + new(30, 37, "305954156"), + new(48, 57, "114870"), + new(63, 70, "38586") + ]; + ImmutableList occurrences = TwitchEventSubChat.ParseEmotes(emotes, message); + ImmutableList expected = [ + new("25", "Kappa"), + new("22639", "BabyRage"), + new("305954156", "PogChamp"), + new("114870", "RaccAttack"), + new("38586", "PraiseIt") + ]; + Assert.That(occurrences, Is.EquivalentTo(expected)); + } +}