Skip to content

Commit

Permalink
add global and TPP emotes back to donations via cached lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
Felk committed Dec 20, 2024
1 parent 34e2084 commit 2fc32fe
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 38 deletions.
6 changes: 3 additions & 3 deletions TPP.Core/Chat/TwitchEventSubChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
using TPP.Common.Utils;
using TPP.Core.Overlay;
using TPP.Core.Overlay.Events;
using TPP.Core.Overlay.Events.Common;
using TPP.Core.Utils;
using TPP.Model;
using TPP.Persistence;
using TPP.Twitch.EventSub;
using TPP.Twitch.EventSub.Notifications;
Expand Down Expand Up @@ -508,7 +508,7 @@ private async Task ChannelSubscribeReceived(ChannelSubscribe channelSubscribe)
await _overlayConnection.Send(new NewSubscriber
{
User = subscriptionInfo.Subscriber,
Emotes = subscriptionInfo.Emotes.Select(EmoteInfo.FromOccurence).ToImmutableList(),
Emotes = subscriptionInfo.Emotes.Select(e => EmoteInfo.FromIdAndCode(e.Id, e.Code)).ToImmutableList(),
SubMessage = subscriptionInfo.Message,
ShareSub = true,
}, CancellationToken.None);
Expand Down Expand Up @@ -546,7 +546,7 @@ private async Task ChannelSubscriptionMessageReceived(ChannelSubscriptionMessage
await _overlayConnection.Send(new NewSubscriber
{
User = subscriptionInfo.Subscriber,
Emotes = subscriptionInfo.Emotes.Select(EmoteInfo.FromOccurence).ToImmutableList(),
Emotes = subscriptionInfo.Emotes.Select(e => EmoteInfo.FromIdAndCode(e.Id, e.Code)).ToImmutableList(),
SubMessage = subscriptionInfo.Message,
ShareSub = true,
}, CancellationToken.None);
Expand Down
1 change: 1 addition & 0 deletions TPP.Core/Configuration/ConnectionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public enum SuppressionType { Whisper, Message, Command }
public CaseInsensitiveImmutableHashSet SuppressionOverrides { get; init; } = new([]);

public Duration? GetChattersInterval { get; init; } = Duration.FromMinutes(5);
public Duration? GetEmotesInterval { get; init; } = Duration.FromHours(1);
}

public sealed class Simulation : ConnectionConfig
Expand Down
5 changes: 2 additions & 3 deletions TPP.Core/DonationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class DonationHandler(
IMessageSender messageSender,
OverlayConnection overlayConnection,
IChattersSnapshotsRepo chattersSnapshotsRepo,
TwitchEmotesLookup? twitchEmotesLookup,
int centsPerToken,
int donorBadgeCents)
{
Expand Down Expand Up @@ -88,9 +89,7 @@ await donationRepo.InsertDonation(
await RandomlyDistributeTokens(donation.CreatedAt, donation.Id, donation.Username, tokens.Total());
await overlayConnection.Send(new NewDonationEvent
{
// We used to look up emotes using the internal Emote Service, but this small feature (emotes in donations)
// was the only thing remaining using the Emote Service, so it's not worth it.
Emotes = [],
Emotes = twitchEmotesLookup?.FindEmotesInText(donation.Message ?? "") ?? [],
RecordDonations = new Dictionary<int, List<string>>
{
[cents] = recordBreaks.Select(recordBreak => recordBreak.Name).ToList()
Expand Down
32 changes: 25 additions & 7 deletions TPP.Core/Modes/ModeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public sealed class ModeBase : IWithLifecycle, ICommandHandler
private readonly IClock _clock;
private readonly ProcessMessage _processMessage;
private readonly ChattersWorker? _chattersWorker;
private readonly TwitchEmotesLookup? _emotesWorker;
private readonly DonationsWorker? _donationsWorker;

/// Processes a message that wasn't already processed by the mode base,
Expand Down Expand Up @@ -141,20 +142,35 @@ public ModeBase(
}
}

// chatters worker
List<ConnectionConfig.Twitch> chatsWithChattersWorker = baseConfig.Chat.Connections
.OfType<ConnectionConfig.Twitch>()
.Where(con => con.GetChattersInterval != null)
.ToList();
ConnectionConfig.Twitch? primaryChat = chatsWithChattersWorker.FirstOrDefault();
ConnectionConfig.Twitch? primaryChattersChat = chatsWithChattersWorker.FirstOrDefault();
if (chatsWithChattersWorker.Count > 1)
_logger.LogWarning("More than one twitch chat have GetChattersInterval configured: {ChatNames}. " +
"Using only the first one ('{ChosenChat}') for the chatters worker",
string.Join(", ", chatsWithChattersWorker.Select(c => c.Name)), primaryChat?.Name);
_chattersWorker = primaryChat == null
string.Join(", ", chatsWithChattersWorker.Select(c => c.Name)), primaryChattersChat?.Name);
_chattersWorker = primaryChattersChat == null
? null
: new ChattersWorker(loggerFactory, clock,
((TwitchChat)_chats[primaryChat.Name]).TwitchApi, repos.ChattersSnapshotsRepo, primaryChat,
repos.UserRepo);
: new ChattersWorker(loggerFactory, clock, ((TwitchChat)_chats[primaryChattersChat.Name]).TwitchApi,
repos.ChattersSnapshotsRepo, primaryChattersChat, repos.UserRepo);

// emotes lookup worker
List<ConnectionConfig.Twitch> chatsWithEmotesWorker = baseConfig.Chat.Connections
.OfType<ConnectionConfig.Twitch>()
.Where(con => con.GetEmotesInterval != null)
.ToList();
ConnectionConfig.Twitch? primaryEmotesChat = chatsWithEmotesWorker.FirstOrDefault();
if (chatsWithEmotesWorker.Count > 1)
_logger.LogWarning("More than one twitch chat have GetEmotesInterval configured: {ChatNames}. " +
"Only retrieving global and that channel's ('{ChosenChat}') sub emotes.",
string.Join(", ", chatsWithEmotesWorker.Select(c => c.Name)), primaryEmotesChat?.Name);
_emotesWorker = primaryEmotesChat == null
? null
: new TwitchEmotesLookup(loggerFactory, ((TwitchChat)_chats[primaryEmotesChat.Name]).TwitchApi,
primaryEmotesChat);

StreamlabsConfig streamlabsConfig = baseConfig.StreamlabsConfig;
if (streamlabsConfig.Enabled)
Expand All @@ -170,7 +186,7 @@ public ModeBase(
_logger.LogWarning("Multiple chats configured, using {Chat} for donation token whispers", chatName);
DonationHandler donationHandler = new(loggerFactory.CreateLogger<DonationHandler>(),
repos.DonationRepo, repos.UserRepo, repos.TokensBank, chat, overlayConnection,
repos.ChattersSnapshotsRepo,
repos.ChattersSnapshotsRepo, _emotesWorker,
centsPerToken: baseConfig.CentsPerToken, donorBadgeCents: baseConfig.DonorBadgeCents);
StreamlabsClient streamlabsClient = new(loggerFactory.CreateLogger<StreamlabsClient>(),
streamlabsConfig.AccessToken);
Expand Down Expand Up @@ -275,6 +291,8 @@ public async Task Start(CancellationToken cancellationToken)
tasks.Add(_sendOutQueuedMessagesWorker.Start(cancellationToken));
if (_chattersWorker != null)
tasks.Add(_chattersWorker.Start(cancellationToken));
if (_emotesWorker != null)
tasks.Add(_emotesWorker.Start(cancellationToken));
if (_donationsWorker != null)
tasks.Add(_donationsWorker.Start(cancellationToken));
await TaskUtils.WhenAllFastExit(tasks);
Expand Down
23 changes: 0 additions & 23 deletions TPP.Core/Overlay/Events/Common/EmoteInfo.cs

This file was deleted.

2 changes: 1 addition & 1 deletion TPP.Core/Overlay/Events/NewDonationEvent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using TPP.Core.Overlay.Events.Common;
using TPP.Model;

namespace TPP.Core.Overlay.Events;

Expand Down
1 change: 0 additions & 1 deletion TPP.Core/Overlay/Events/SubscriptionEvents.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Immutable;
using System.Runtime.Serialization;
using TPP.Core.Overlay.Events.Common;
using TPP.Model;

namespace TPP.Core.Overlay.Events
Expand Down
6 changes: 6 additions & 0 deletions TPP.Core/TwitchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using TwitchLib.Api.Core.Enums;
using TwitchLib.Api.Core.Exceptions;
using TwitchLib.Api.Helix.Models.Chat.ChatSettings;
using TwitchLib.Api.Helix.Models.Chat.Emotes.GetChannelEmotes;
using TwitchLib.Api.Helix.Models.Chat.Emotes.GetGlobalEmotes;
using TwitchLib.Api.Helix.Models.Chat.GetChatters;
using TwitchLib.Api.Helix.Models.EventSub;
using TwitchLib.Api.Helix.Models.Moderation.BanUser;
Expand Down Expand Up @@ -96,6 +98,10 @@ public Task SendChatMessage(string broadcasterId, string senderUserId, string me
replyParentMessageId: replyParentMessageId));
public Task SendWhisperAsync(string fromUserId, string toUserId, string message, bool newRecipient) =>
RetryingBot(api => api.Helix.Whispers.SendWhisperAsync(fromUserId, toUserId, message, newRecipient));
public Task<GetGlobalEmotesResponse> GetGlobalEmotes() =>
RetryingBot(api => api.Helix.Chat.GetGlobalEmotesAsync());
public Task<GetChannelEmotesResponse> GetChannelEmotes(string broadcasterId) =>
RetryingBot(api => api.Helix.Chat.GetChannelEmotesAsync(broadcasterId));

// Users
public Task<GetUsersResponse> GetUsersAsync(List<string> ids) =>
Expand Down
83 changes: 83 additions & 0 deletions TPP.Core/TwitchEmotesLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using TPP.Core.Configuration;
using TPP.Model;
using TwitchLib.Api.Helix.Models.Chat.Emotes;

namespace TPP.Core;

public class TwitchEmotesLookup(
ILoggerFactory loggerFactory,
TwitchApi twitchApi,
ConnectionConfig.Twitch chatConfig
) : IWithLifecycle
{
private readonly ILogger<TwitchEmotesLookup> _logger = loggerFactory.CreateLogger<TwitchEmotesLookup>();
private readonly TimeSpan _refreshInterval = chatConfig.GetEmotesInterval!.Value.ToTimeSpan();
private Dictionary<string, EmoteInfo> _knownEmotesByCode = [];
private Regex _emoteCodesRegex = new("Kappa");

public List<EmoteInfo> FindEmotesInText(string text)
{
MatchCollection matches = _emoteCodesRegex.Matches(text);
return matches.Select(match => _knownEmotesByCode[match.Value]).ToList();
}

private async Task RenewEmotes()
{
Stopwatch stopwatch = new();
stopwatch.Start();
GlobalEmote[] globalEmotes = (await twitchApi.GetGlobalEmotes()).GlobalEmotes;
ChannelEmote[] channelEmotes = (await twitchApi.GetChannelEmotes(chatConfig.ChannelId)).ChannelEmotes;
stopwatch.Stop();
_logger.LogDebug("Retrieved {NumGlobalEmotes} global and {NumChannelEmotes} channel emotes in {ElapsedMs}ms",
globalEmotes.Length, channelEmotes.Length, stopwatch.ElapsedMilliseconds);

IEnumerable<EmoteInfo> globalEmoteInfos = globalEmotes.Select(e => new EmoteInfo
{
Code = e.Name, Id = e.Id,
X1 = e.Images.Url1X, X2 = e.Images.Url2X, X3 = e.Images.Url4X
});
IEnumerable<EmoteInfo> channelEmoteInfos = channelEmotes.Select(e => new EmoteInfo
{
Code = e.Name, Id = e.Id,
X1 = e.Images.Url1X, X2 = e.Images.Url2X, X3 = e.Images.Url4X
});

// Don't use ToDictionary, which forbids duplicate keys, because emote codes are not unique.
// E.g. ':D' can either be ID 3 or 555555560, but they look identical anyway.
Dictionary<string, EmoteInfo> knownEmotesByCode = [];
foreach (EmoteInfo emoteInfo in globalEmoteInfos)
knownEmotesByCode[emoteInfo.Code] = emoteInfo;
foreach (EmoteInfo emoteInfo in channelEmoteInfos)
knownEmotesByCode[emoteInfo.Code] = emoteInfo;
_knownEmotesByCode = knownEmotesByCode;
_emoteCodesRegex = new Regex(string.Join('|', _knownEmotesByCode.Keys.Select(Regex.Escape)));
_logger.LogDebug("New emotes list: {Emotes}", string.Join(", ", _knownEmotesByCode.Keys));
}

public async Task Start(CancellationToken cancellationToken)
{
// don't wait at startup, refresh right away
while (!cancellationToken.IsCancellationRequested)
{
try
{
await RenewEmotes();
}
catch (Exception e)
{
_logger.LogError(e, "Failed renewing emotes");
}

try { await Task.Delay(_refreshInterval, cancellationToken); }
catch (OperationCanceledException) { break; }
}
}
}
26 changes: 26 additions & 0 deletions TPP.Model/EmoteInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Runtime.Serialization;

namespace TPP.Model;

[DataContract]
public struct EmoteInfo
{
[DataMember(Name = "id")] public string Id { get; set; }
[DataMember(Name = "code")] public string Code { get; set; }
[DataMember(Name = "x1")] public string X1 { get; set; }
[DataMember(Name = "x2")] public string X2 { get; set; }
[DataMember(Name = "x3")] public string X3 { get; set; }

public static EmoteInfo FromIdAndCode(string id, string code) => new()
{
Code = code,
Id = id,
// see https://dev.twitch.tv/docs/irc/tags#privmsg-twitch-tags
X1 = $"https://static-cdn.jtvnw.net/emoticons/v2/{id}/static/light/1.0",
X2 = $"https://static-cdn.jtvnw.net/emoticons/v2/{id}/static/light/2.0",
X3 = $"https://static-cdn.jtvnw.net/emoticons/v2/{id}/static/light/3.0",
};

public override string ToString() =>
$"Emote({nameof(Id)}: {Id}, {nameof(Code)}: {Code})";
}

0 comments on commit 2fc32fe

Please sign in to comment.