diff --git a/Chat/Commands/TTSCommand.cs b/Chat/Commands/TTSCommand.cs index f7c2c64..68b68a6 100644 --- a/Chat/Commands/TTSCommand.cs +++ b/Chat/Commands/TTSCommand.cs @@ -214,27 +214,32 @@ namespace TwitchChatTTS.Chat.Commands return; } - string targetUserId = fragment.Mention!.UserId!; + string targetUserId = fragment.Mention!.UserId; if (targetUserId == _user.TwitchUserId.ToString()) { _logger.Warning("Cannot join yourself."); return; } + string targetUserLogin = fragment.Mention!.UserLogin; string[] subscriptions = ["channel.chat.message", "channel.chat.message_delete", "channel.chat.clear_user_messages"]; foreach (var subscription in subscriptions) + await Subscribe(subscription, targetUserId, targetUserLogin, async () => await _client.CreateEventSubscription(subscription, "1", _twitch.SessionId, _user.TwitchUserId.ToString(), targetUserId)); + await Subscribe("channel.raid", targetUserId, targetUserLogin, async () => await _client.CreateChannelRaidEventSubscription("1", _twitch.SessionId, targetUserId)); + _logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel: {targetUserLogin}][channel id: {targetUserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]"); + } + + private async Task Subscribe(string subscription, string targetId, string targetName, Func?>> subscribe) + { + _logger.Debug($"Attempting to subscribe to Twitch events [subscription: {subscription}][target channel: {targetName}][target channel id: {targetId}]"); + var data = await subscribe.Invoke(); + var info = data?.Data?.FirstOrDefault(); + if (info == null) { - _logger.Debug($"Attempting to subscribe to Twitch events [subscription: {subscription}]"); - var data = await _client.CreateEventSubscription(subscription, "1", _twitch.SessionId, _user.TwitchUserId.ToString(), targetUserId); - var info = data?.Data?.FirstOrDefault(); - if (info == null) - { - _logger.Warning("Could not find the subscription id."); - continue; - } - _twitch.AddSubscription(targetUserId, subscription, info.Id); + _logger.Warning("Could not find the subscription id."); + return; } - _logger.Information($"Joined chat room [channel: {fragment.Mention.UserLogin}][channel id: {targetUserId}][invoker: {message.ChatterUserLogin}][id: {message.ChatterUserId}]"); + _twitch.AddSubscription(targetId, subscription, info.Id); } } @@ -277,7 +282,7 @@ namespace TwitchChatTTS.Chat.Commands return; } - string[] subscriptions = ["channel.chat.message", "channel.chat.message_delete", "channel.chat.clear_user_messages"]; + string[] subscriptions = ["channel.chat.message", "channel.chat.message_delete", "channel.chat.clear_user_messages", "channel.raid"]; foreach (var subscription in subscriptions) { var subscriptionId = _twitch.GetSubscriptionId(targetUserId, subscription); diff --git a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs index fc6e002..295c24c 100644 --- a/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs +++ b/Twitch/Socket/Handlers/ChannelChatMessageHandler.cs @@ -98,12 +98,6 @@ 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); @@ -166,6 +160,12 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers } } + if (_user.Raids.TryGetValue(message.BroadcasterUserId, out var raid) && !raid.Chatters.Contains(chatterId)) + { + _logger.Information($"Potential chat message from raider ignored due to potential raid message spam [chatter: {message.ChatterUserLogin}][chatter id: {message.ChatterUserId}]"); + return; + } + // Replace filtered words. if (_user.RegexFilters != null) { diff --git a/Twitch/Socket/Handlers/ChannelRaidHandler.cs b/Twitch/Socket/Handlers/ChannelRaidHandler.cs index a3eb465..c3613fb 100644 --- a/Twitch/Socket/Handlers/ChannelRaidHandler.cs +++ b/Twitch/Socket/Handlers/ChannelRaidHandler.cs @@ -26,40 +26,67 @@ namespace TwitchChatTTS.Twitch.Socket.Handlers return; _logger.Information($"A raid has started. Starting raid spam prevention. [from: {message.FromBroadcasterUserLogin}][from id: {message.FromBroadcasterUserId}]."); - var chatters = await _api.GetChatters(_user.TwitchUserId.ToString(), _user.TwitchUserId.ToString()); - if (chatters?.Data == null) - { - _logger.Error("Could not fetch the list of chatters in chat."); - return; - } + EventResponse? chatters = null; - var date = DateTime.Now; - lock (_lock) + if (!_user.Raids.ContainsKey(message.ToBroadcasterUserId)) { - _user.RaidStart = date; - if (_user.AllowedChatters == null) + await _api.GetChatters(_user.TwitchUserId.ToString(), _user.TwitchUserId.ToString()); + if (chatters?.Data == null) { - var chatterIds = chatters.Data.Select(c => long.Parse(c.UserId)); - _user.AllowedChatters = new HashSet(chatterIds); + var extraErrorInfo = _user.TwitchUserId.ToString() != message.ToBroadcasterUserId ? " Ensure you have moderator status in your joined channel(s) to prevent raid spam." : string.Empty; + _logger.Error("Could not fetch the list of chatters in chat." + extraErrorInfo); + return; } } - Task.Run(async () => await EndOfRaidSpamProtection(date)); + var startDate = DateTime.Now; + var endDate = startDate + TimeSpan.FromSeconds(30); + lock (_lock) + { + if (_user.Raids.TryGetValue(message.ToBroadcasterUserId, out var raid)) + raid.RaidSpamPreventionEndDate = endDate; + else + { + var chatterIds = chatters!.Data!.Select(c => long.Parse(c.UserId)); + _user.Raids.Add(message.ToBroadcasterUserId, new RaidInfo(endDate, new HashSet(chatterIds))); + } + } + + Task.Run(async () => await EndOfRaidSpamProtection(message.ToBroadcasterUserId, endDate)); } - private async Task EndOfRaidSpamProtection(DateTime date) + private async Task EndOfRaidSpamProtection(string raidedId, DateTime endDate) { - await Task.Delay(TimeSpan.FromSeconds(30)); + await Task.Delay(endDate - DateTime.Now); lock (_lock) { - if (_user.RaidStart == date) + if (_user.Raids.TryGetValue(raidedId, out var raid)) { - _logger.Information("Raid message spam prevention ended."); - _user.RaidStart = null; - _user.AllowedChatters = null; + if (raid.RaidSpamPreventionEndDate == endDate) + { + _logger.Information("Raid message spam prevention ended."); + _user.Raids.Remove(raidedId); + } + else + _logger.Debug("Raid spam prevention would have stopped now if it wasn't for the consecutive raid."); } + else + _logger.Error("Something went wrong ending a raid spam prevention."); } } } + + public sealed class RaidInfo + { + public DateTime RaidSpamPreventionEndDate { get; set; } + public HashSet Chatters { get; set; } + + + public RaidInfo(DateTime raidEnd, HashSet chatters) + { + RaidSpamPreventionEndDate = raidEnd; + Chatters = chatters; + } + } } \ No newline at end of file diff --git a/Twitch/TwitchApiClient.cs b/Twitch/TwitchApiClient.cs index 35db407..6479230 100644 --- a/Twitch/TwitchApiClient.cs +++ b/Twitch/TwitchApiClient.cs @@ -28,7 +28,7 @@ public class TwitchApiClient }); } - public async Task?> CreateEventSubscription(string type, string version, string sessionId, IDictionary conditions) + private async Task?> CreateEventSubscription(string type, string version, string sessionId, IDictionary conditions) { var subscriptionData = new EventSubscriptionMessage(type, version, sessionId, conditions); var base_url = _configuration.Environment == "PROD" || string.IsNullOrWhiteSpace(_configuration.Twitch?.ApiUrl) @@ -58,7 +58,7 @@ public class TwitchApiClient conditions.Add("from_broadcaster_user_id", from); if (to != null) conditions.Add("to_broadcaster_user_id", to); - + return await CreateEventSubscription("channel.raid", version, sessionId, conditions); } diff --git a/User.cs b/User.cs index ceb44a9..e00acb1 100644 --- a/User.cs +++ b/User.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; using HermesSocketLibrary.Requests.Messages; +using TwitchChatTTS.Twitch.Socket.Handlers; namespace TwitchChatTTS { @@ -22,8 +23,7 @@ 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 IDictionary Raids { get; set; } = new Dictionary(); public HashSet Chatters { get; set; } public TTSWordFilter[] RegexFilters { get; set; } [JsonIgnore]