Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix sub emote indices #453

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions TPP.Core/Chat/TwitchEventSubChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <see cref="ChannelSubscriptionMessage.Emote"/>.
/// 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<EmoteOccurrence> 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;
Expand All @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion TPP.Core/Subscriptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <c>int StartIndex</c> and <c>int EndIndex</c>, but since those were unused and how
/// to count was ambiguous (see <see cref="ChannelSubscriptionMessage.Emote"/>), they were removed.
public record EmoteOccurrence(string Id, string Code);

/// <summary>
/// Information on a user subscription directly (not via a gift).
Expand Down
13 changes: 13 additions & 0 deletions TPP.Twitch.EventSub/Notifications/ChannelSubscriptionMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ public record Condition(string BroadcasterUserId) : EventSub.Condition;
/// <param name="Begin">The index of where the Emote starts in the text.</param>
/// <param name="End">The index of where the Emote ends in the text.</param>
/// <param name="Id">The emote ID.</param>
/// 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:
/// <code>test Kappa 🌿 BabyRage 🐍 PogChamp 🎅🏿 RaccAttack ♥ PraiseIt 12345678901234567890</code>
/// resulted in these emote indices:
/// <code>
/// {"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"}
/// </code>
public record Emote(
int Begin,
int End,
Expand Down
38 changes: 38 additions & 0 deletions tests/TPP.Core.Tests/Chat/TwitchEventSubChatTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The data comes from an actual subscription that was observed.
/// See also <see cref="ChannelSubscriptionMessage.Emote"/>
/// </summary>
[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<EmoteOccurrence> occurrences = TwitchEventSubChat.ParseEmotes(emotes, message);
ImmutableList<EmoteOccurrence> expected = [
new("25", "Kappa"),
new("22639", "BabyRage"),
new("305954156", "PogChamp"),
new("114870", "RaccAttack"),
new("38586", "PraiseIt")
];
Assert.That(occurrences, Is.EquivalentTo(expected));
}
}
Loading