From e4a11382effa7f2294d931bbb65d07d4849d5aac Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 7 Aug 2024 20:30:03 +0000 Subject: [PATCH] Added more information to logs when receiving subscriptions. Added raid message spam prevention. Added bit message detection - requires tts.chat.bits.read permission for TTS." --- .../Handlers/ChannelChatMessageHandler.cs | 13 +++- Twitch/Socket/Handlers/ChannelRaidHandler.cs | 59 +++++++++++++++++++ .../Handlers/ChannelResubscriptionHandler.cs | 4 +- .../ChannelSubscriptionGiftHandler.cs | 8 ++- .../Handlers/ChannelSubscriptionHandler.cs | 4 +- .../Socket/Handlers/SessionWelcomeHandler.cs | 11 +--- Twitch/Socket/Messages/ChannelRaidMessage.cs | 13 ++++ .../Messages/ChannelSubscriptionMessage.cs | 6 +- Twitch/Socket/Messages/ChatterMessage.cs | 9 +++ Twitch/TwitchApiClient.cs | 13 ++++ User.cs | 2 + 11 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 Twitch/Socket/Handlers/ChannelRaidHandler.cs create mode 100644 Twitch/Socket/Messages/ChannelRaidMessage.cs create mode 100644 Twitch/Socket/Messages/ChatterMessage.cs diff --git a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs index 1503f4d..fc6e002 100644 --- a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs +++ b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs @@ -77,7 +77,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers return; // new MessageResult(MessageStatus.NotReady, -1, -1); } - var msg = message.Message.Text; + var msg = string.Join(string.Empty, message.Message.Fragments.Where(f => f.Type != "cheermote").Select(f => f.Text)).Trim(); var chatterId = long.Parse(message.ChatterUserId); var tasks = new List(); @@ -98,12 +98,23 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers return; } + if (_user.AllowedChatters != null && !_user.AllowedChatters.Contains(chatterId)) + { + _logger.Information("Potential chat message from raider ignored due to potential raid message spam."); + return; + } + if (message.Reply != null) msg = msg.Substring(message.Reply.ParentUserLogin.Length + 2); + var bits = message.Message.Fragments.Where(f => f.Type == "cheermote" && f.Cheermote != null) + .Select(f => f.Cheermote!.Bits) + .Sum(); var permissionPath = "tts.chat.messages.read"; if (!string.IsNullOrWhiteSpace(message.ChannelPointsCustomRewardId)) permissionPath = "tts.chat.redemptions.read"; + else if (bits > 0) + permissionPath = "tts.chat.bits.read"; var permission = chatterId == _user.OwnerId ? true : _permissionManager.CheckIfAllowed(groups, permissionPath); if (permission != true) diff --git a/Twitch/Socket/Handlers/ChannelRaidHandler.cs b/Twitch/Socket/Handlers/ChannelRaidHandler.cs new file mode 100644 index 0000000..40cde62 --- /dev/null +++ b/Twitch/Socket/Handlers/ChannelRaidHandler.cs @@ -0,0 +1,59 @@ +using Serilog; +using TwitchChatTTS.Twitch.Socket.Messages; + +namespace TwitchChatTTS.Twitch.Socket.Handlers +{ + public class ChannelRaidHandler : ITwitchSocketHandler + { + public string Name => "channel.raid"; + + private readonly TwitchApiClient _api; + private readonly User _user; + private readonly ILogger _logger; + private readonly object _lock; + + public ChannelRaidHandler(TwitchApiClient api, User user, ILogger logger) + { + _api = api; + _user = user; + _logger = logger; + _lock = new object(); + } + + public async Task Execute(TwitchWebsocketClient sender, object data) + { + if (data is not ChannelRaidMessage message) + return; + + var chatters = await _api.GetChatters(message.ToBroadcasterUserId, message.ToBroadcasterUserLogin); + if (chatters?.Data == null) + { + _logger.Error("Could not fetch the list of chatters in chat."); + return; + } + + var date = DateTime.Now; + lock (_lock) + { + _user.RaidStart = date; + if (_user.AllowedChatters == null) + { + var chatterIds = chatters.Data.Select(c => long.Parse(c.UserId)); + _user.AllowedChatters = new HashSet(chatterIds); + } + } + + await Task.Delay(TimeSpan.FromSeconds(30)); + + lock (_lock) + { + if (_user.RaidStart == date) + { + _logger.Information("Raid message spam prevention ended."); + _user.RaidStart = null; + _user.AllowedChatters = null; + } + } + } + } +} \ No newline at end of file diff --git a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs index efeec08..4fb96e9 100644 --- a/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelResubscriptionHandler.cs @@ -22,7 +22,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers if (data is not ChannelResubscriptionMessage message) return; - _logger.Debug("Resubscription occured."); + _logger.Debug($"Resubscription occured [chatter: {message.UserLogin}][chatter id: {message.UserId}][Tier: {message.Tier}][Streak: {message.StreakMonths}][Cumulative: {message.CumulativeMonths}][Duration: {message.DurationMonths}]"); try { var actions = _redemptionManager.Get("subscription"); @@ -36,7 +36,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId)); + await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!)); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs index c4025d4..9a4ea23 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionGiftHandler.cs @@ -22,7 +22,11 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers if (data is not ChannelSubscriptionGiftMessage message) return; - _logger.Debug("Gifted subscription occured."); + if (message.IsAnonymous) + _logger.Debug($"Gifted subscription occured [chatter: Anonymous][Tier: {message.Tier}][Count: {message.Total}]"); + else + _logger.Debug($"Gifted subscription occured [chatter: {message.UserLogin}][chatter id: {message.UserId}][Tier: {message.Tier}][Count: {message.Total}][Cumulative Count: {message.CumulativeTotal}]"); + try { var actions = _redemptionManager.Get("subscription.gift"); @@ -36,7 +40,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId)); + await _redemptionManager.Execute(action, message.UserName ?? "Anonymous", message.UserId == null ? 0 : long.Parse(message.UserId)); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs index c93abe8..878c4d2 100644 --- a/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs +++ b/Twitch/Socket/Handlers/ChannelSubscriptionHandler.cs @@ -24,7 +24,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers if (message.IsGifted) return; - _logger.Debug("Subscription occured."); + _logger.Debug($"Subscription occured [chatter: {message.UserLogin}][chatter id: {message.UserId}][Tier: {message.Tier}]"); try { var actions = _redemptionManager.Get("subscription"); @@ -38,7 +38,7 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers foreach (var action in actions) try { - await _redemptionManager.Execute(action, message.UserName, long.Parse(message.UserId)); + await _redemptionManager.Execute(action, message.UserName!, long.Parse(message.UserId!)); } catch (Exception ex) { diff --git a/Twitch/Socket/Handlers/SessionWelcomeHandler.cs b/Twitch/Socket/Handlers/SessionWelcomeHandler.cs index 35f4932..e92e4c1 100644 --- a/Twitch/Socket/Handlers/SessionWelcomeHandler.cs +++ b/Twitch/Socket/Handlers/SessionWelcomeHandler.cs @@ -22,12 +22,8 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers public async Task Execute(TwitchWebsocketClient sender, object data) { - if (sender == null) - return; if (data is not SessionWelcomeMessage message) return; - if (_api == null) - return; if (string.IsNullOrEmpty(message.Session.Id)) { @@ -43,15 +39,15 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers string[] subscriptionsv1 = [ "channel.chat.message", "channel.chat.message_delete", - "channel.chat.notification", "channel.chat.clear", "channel.chat.clear_user_messages", - "channel.ad_break.begin", "channel.subscribe", "channel.subscription.gift", "channel.subscription.message", + "channel.ad_break.begin", "channel.ban", - "channel.channel_points_custom_reward_redemption.add" + "channel.channel_points_custom_reward_redemption.add", + "channel.raid" ]; string[] subscriptionsv2 = [ "channel.follow", @@ -92,7 +88,6 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers var response = await _api.CreateEventSubscription(subscriptionName, version, sessionId, broadcasterId); if (response == null) { - _logger.Error($"Failed to create an event subscription [subscription type: {subscriptionName}][reason: response is null]"); return; } if (response.Data == null) diff --git a/Twitch/Socket/Messages/ChannelRaidMessage.cs b/Twitch/Socket/Messages/ChannelRaidMessage.cs new file mode 100644 index 0000000..0e26d36 --- /dev/null +++ b/Twitch/Socket/Messages/ChannelRaidMessage.cs @@ -0,0 +1,13 @@ +namespace TwitchChatTTS.Twitch.Socket.Messages +{ + public class ChannelRaidMessage + { + public string FromBroadcasterUserId { get; set; } + public string FromBroadcasterUserLogin { get; set; } + public string FromBroadcasterUserName { get; set; } + public string ToBroadcasterUserId { get; set; } + public string ToBroadcasterUserLogin { get; set; } + public string ToBroadcasterUserName { get; set; } + public int Viewers { get; set; } + } +} \ No newline at end of file diff --git a/Twitch/Socket/Messages/ChannelSubscriptionMessage.cs b/Twitch/Socket/Messages/ChannelSubscriptionMessage.cs index d095bf2..ef87e58 100644 --- a/Twitch/Socket/Messages/ChannelSubscriptionMessage.cs +++ b/Twitch/Socket/Messages/ChannelSubscriptionMessage.cs @@ -2,9 +2,9 @@ namespace TwitchChatTTS.Twitch.Socket.Messages { public class ChannelSubscriptionData { - public string UserId { get; set; } - public string UserLogin { get; set; } - public string UserName { get; set; } + public string? UserId { get; set; } + public string? UserLogin { get; set; } + public string? UserName { get; set; } public string BroadcasterUserId { get; set; } public string BroadcasterUserLogin { get; set; } public string BroadcasterUserName { get; set; } diff --git a/Twitch/Socket/Messages/ChatterMessage.cs b/Twitch/Socket/Messages/ChatterMessage.cs new file mode 100644 index 0000000..0e4d879 --- /dev/null +++ b/Twitch/Socket/Messages/ChatterMessage.cs @@ -0,0 +1,9 @@ +namespace TwitchChatTTS.Twitch.Socket.Messages +{ + public class ChatterMessage + { + public string UserId { get; set; } + public string UserLogin { get; set; } + public string UserName { get; set; } + } +} \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 0070100..881008a 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -51,6 +51,19 @@ public class TwitchApiClient await _web.Delete($"{base_url}/eventsub/subscriptions?id=" + subscriptionId); } + public async Task?> GetChatters(string broadcasterId, string? moderatorId = null) + { + moderatorId ??= broadcasterId; + var response = await _web.Get($"https://api.twitch.tv/helix/chat/chatters?broadcaster_id={broadcasterId}&moderator_id={moderatorId}"); + if (response.StatusCode == HttpStatusCode.Accepted) + { + _logger.Debug($"Twitch API call [type: get chatters][response: {await response.Content.ReadAsStringAsync()}]"); + return await response.Content.ReadFromJsonAsync(typeof(EventResponse)) as EventResponse; + } + _logger.Error($"Twitch API call failed [type: get chatters][response: {await response.Content.ReadAsStringAsync()}]"); + return null; + } + public async Task?> GetSubscriptions(string? status = null, string? broadcasterId = null, string? after = null) { List queryParams = new List(); diff --git a/User.cs b/User.cs index 66f6501..ceb44a9 100644 --- a/User.cs +++ b/User.cs @@ -22,6 +22,8 @@ namespace TwitchChatTTS // voice names public HashSet VoicesEnabled { get => _voicesEnabled; set { _voicesEnabled = value; VoiceNameRegex = GenerateEnabledVoicesRegex(); } } + public DateTime? RaidStart { get; set; } + public HashSet? AllowedChatters { get; set; } public HashSet Chatters { get; set; } public TTSWordFilter[] RegexFilters { get; set; } [JsonIgnore]