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));
+ }
+}