From 42d5ccf1413e3e0bd5b1cd6a0bbd9761983bc21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Sun, 2 Feb 2025 16:36:20 -0500 Subject: [PATCH 1/3] chore: moving model to models folder --- .../ChatRules/AChatRuleUnitTestBase.cs | 4 +++- .../ChatRules/BestCheapViewersTests.cs | 1 + src/Nullinside.Api.TwitchBot.Tests/ChatRules/DiscordTests.cs | 1 + src/Nullinside.Api.TwitchBot.Tests/ChatRules/DoghypeTests.cs | 1 + src/Nullinside.Api.TwitchBot.Tests/ChatRules/NezhnaTests.cs | 1 + .../ChatRules/StreamViewersTests.cs | 1 + src/Nullinside.Api.TwitchBot/ChatRules/BestCheapViewers.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/Discord.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/Dogehype.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/HostHub.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/IChatRule.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/IfYouWantViewers.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/Naked.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/Nezhna.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/StreamRise.cs | 4 +++- src/Nullinside.Api.TwitchBot/ChatRules/StreamViewers.cs | 4 +++- .../{ChatRules => Model}/TwitchChatMessage.cs | 2 +- 17 files changed, 39 insertions(+), 12 deletions(-) rename src/Nullinside.Api.TwitchBot/{ChatRules => Model}/TwitchChatMessage.cs (97%) diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/AChatRuleUnitTestBase.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/AChatRuleUnitTestBase.cs index 8a76b16..50f73a9 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/AChatRuleUnitTestBase.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/AChatRuleUnitTestBase.cs @@ -1,8 +1,10 @@ using Moq; using Nullinside.Api.Common.Twitch; -using Nullinside.Api.Model.Ddl; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; + +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/BestCheapViewersTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/BestCheapViewersTests.cs index 9f57cfc..88007ed 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/BestCheapViewersTests.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/BestCheapViewersTests.cs @@ -2,6 +2,7 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DiscordTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DiscordTests.cs index d309d7f..6c99848 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DiscordTests.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DiscordTests.cs @@ -2,6 +2,7 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DoghypeTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DoghypeTests.cs index d628e34..e1e71ce 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DoghypeTests.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/DoghypeTests.cs @@ -2,6 +2,7 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/NezhnaTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/NezhnaTests.cs index e2c3f2d..a6606a7 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/NezhnaTests.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/NezhnaTests.cs @@ -2,6 +2,7 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/StreamViewersTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/StreamViewersTests.cs index 7787e77..fd6cea7 100644 --- a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/StreamViewersTests.cs +++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/StreamViewersTests.cs @@ -2,6 +2,7 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.TwitchBot.ChatRules; +using Nullinside.Api.TwitchBot.Model; namespace Nullinside.Api.TwitchBot.Tests.ChatRules; diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/BestCheapViewers.cs b/src/Nullinside.Api.TwitchBot/ChatRules/BestCheapViewers.cs index 4695f4b..5af5588 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/BestCheapViewers.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/BestCheapViewers.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/Discord.cs b/src/Nullinside.Api.TwitchBot/ChatRules/Discord.cs index f421c0e..54708d5 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/Discord.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/Discord.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/Dogehype.cs b/src/Nullinside.Api.TwitchBot/ChatRules/Dogehype.cs index e0e9904..782bad3 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/Dogehype.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/Dogehype.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/HostHub.cs b/src/Nullinside.Api.TwitchBot/ChatRules/HostHub.cs index dff2cac..febb136 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/HostHub.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/HostHub.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/IChatRule.cs b/src/Nullinside.Api.TwitchBot/ChatRules/IChatRule.cs index 7832784..e639a70 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/IChatRule.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/IChatRule.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/IfYouWantViewers.cs b/src/Nullinside.Api.TwitchBot/ChatRules/IfYouWantViewers.cs index 262684f..bc46a65 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/IfYouWantViewers.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/IfYouWantViewers.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/Naked.cs b/src/Nullinside.Api.TwitchBot/ChatRules/Naked.cs index 63d40b4..321a683 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/Naked.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/Naked.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/Nezhna.cs b/src/Nullinside.Api.TwitchBot/ChatRules/Nezhna.cs index 4548584..d15cb42 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/Nezhna.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/Nezhna.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/StreamRise.cs b/src/Nullinside.Api.TwitchBot/ChatRules/StreamRise.cs index 17ee45d..ac96045 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/StreamRise.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/StreamRise.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/StreamViewers.cs b/src/Nullinside.Api.TwitchBot/ChatRules/StreamViewers.cs index 1ccb640..1919d92 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/StreamViewers.cs +++ b/src/Nullinside.Api.TwitchBot/ChatRules/StreamViewers.cs @@ -1,9 +1,11 @@ using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; +using Nullinside.Api.TwitchBot.Model; using TwitchLib.Client.Models; +using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; + namespace Nullinside.Api.TwitchBot.ChatRules; /// diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/TwitchChatMessage.cs b/src/Nullinside.Api.TwitchBot/Model/TwitchChatMessage.cs similarity index 97% rename from src/Nullinside.Api.TwitchBot/ChatRules/TwitchChatMessage.cs rename to src/Nullinside.Api.TwitchBot/Model/TwitchChatMessage.cs index 8adb12c..9ec9701 100644 --- a/src/Nullinside.Api.TwitchBot/ChatRules/TwitchChatMessage.cs +++ b/src/Nullinside.Api.TwitchBot/Model/TwitchChatMessage.cs @@ -2,7 +2,7 @@ using TwitchLib.Client.Models; -namespace Nullinside.Api.TwitchBot.ChatRules; +namespace Nullinside.Api.TwitchBot.Model; /// /// This wrapper exists for unit testing purposes. It's nice passing the around only because From 9ae0c72ca7b393b5df90cfe9b32864e02d400df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Sun, 2 Feb 2025 16:36:53 -0500 Subject: [PATCH 2/3] feat: disabling known bots --- .../Bots/BanKnownBots.cs | 396 +++++++++--------- 1 file changed, 198 insertions(+), 198 deletions(-) diff --git a/src/Nullinside.Api.TwitchBot/Bots/BanKnownBots.cs b/src/Nullinside.Api.TwitchBot/Bots/BanKnownBots.cs index c1dfb4b..65035e8 100644 --- a/src/Nullinside.Api.TwitchBot/Bots/BanKnownBots.cs +++ b/src/Nullinside.Api.TwitchBot/Bots/BanKnownBots.cs @@ -1,198 +1,198 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; - -using Newtonsoft.Json; - -using Nullinside.Api.Common.Twitch; -using Nullinside.Api.Model; -using Nullinside.Api.Model.Ddl; -using Nullinside.Api.TwitchBot.Model; - -using TwitchLib.Api.Helix.Models.Chat.GetChatters; - -using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; - -namespace Nullinside.Api.TwitchBot.Bots; - -/// -/// Searches for and bans known bots when they enter the chat. -/// -public class BanKnownBots : ABotRule { - /// - /// Handles refreshing the collection. - /// - private static Task _ = Task.Run(async () => { - while (true) { - Task?> twitchInsights = GetTwitchInsightsBots(); - Task?> commanderRoot = GetCommanderRootBots(); - await Task.WhenAll(twitchInsights, commanderRoot); - if (null != twitchInsights.Result) { - KnownBotListUsername = twitchInsights.Result; - } - - if (null != commanderRoot.Result) { - KnownBotListUserId = commanderRoot.Result; - } - - // The list is enormous, force garbage collection. - GC.Collect(); - GC.WaitForPendingFinalizers(); - await Task.Delay(TimeSpan.FromMinutes(10)); - } - }); - - /// - /// The cache of known bots. - /// - public static ImmutableHashSet? KnownBotListUsername { get; set; } - - /// - /// The cache of known bots. - /// - public static ImmutableHashSet? KnownBotListUserId { get; set; } - - /// - /// Determine if the rule is enabled in the configuration. - /// - /// The user's configuration. - /// True if it should run, false otherwise. - public override bool ShouldRun(TwitchUserConfig config) { - return config is { Enabled: true, BanKnownBots: true }; - } - - /// - /// Searches for and bans known bots when they enter the chat. - /// - /// The user. - /// The user's configuration. - /// The twitch api authenticated as the bot user. - /// The database. - /// The cancellation token. - public override async Task Handle(User user, TwitchUserConfig config, ITwitchApiProxy botProxy, - INullinsideContext db, CancellationToken stoppingToken = new()) { - if (null == user.TwitchId) { - return; - } - - // TODO: SKIP MODS AND VIPS - - // Get the list of people in the chat. - List? chatters = - (await botProxy.GetChannelUsers(user.TwitchId, Constants.BotId, stoppingToken))?.ToList(); - if (null == chatters || chatters.Count == 0) { - return; - } - - // Perform the comparison in the lock to prevent multithreading issues. - // The collection is extremely large so we do not want to make a copy of it. - var botsInChatInsights = new List(); - var botsInChatCommanderRoot = new List(); - ImmutableHashSet? knownBotUsernames = KnownBotListUsername; - ImmutableHashSet? knownBotUserIds = KnownBotListUserId; - - if (null != knownBotUsernames) { - botsInChatInsights.AddRange(chatters.Where(k => - knownBotUsernames.Contains(k.UserLogin.ToLowerInvariant()))); - } - - if (null != knownBotUserIds) { - botsInChatCommanderRoot.AddRange(chatters.Where(k => - knownBotUserIds.Contains(k.UserId.ToLowerInvariant()))); - } - - // Remove the whitelisted bots - botsInChatInsights = botsInChatInsights - .Where(b => !Constants.WhitelistedBots.Contains(b.UserLogin.ToLowerInvariant())).ToList(); - botsInChatCommanderRoot = botsInChatCommanderRoot - .Where(b => !Constants.WhitelistedBots.Contains(b.UserLogin.ToLowerInvariant())).ToList(); - - // Ban them. - if (botsInChatInsights.Count != 0) { - await BanOnce(botProxy, db, user.TwitchId, botsInChatInsights.Select(b => (Id: b.UserId, Username: b.UserLogin)), - "[Bot] Username on Known Bot List", stoppingToken); - } - - if (botsInChatCommanderRoot.Count != 0) { - await BanOnce(botProxy, db, user.TwitchId, - botsInChatCommanderRoot.Select(b => (Id: b.UserId, Username: b.UserLogin)), - "[Bot] Username on Known Bot List", stoppingToken); - } - } - - /// - /// Gets the list of all known bots. - /// - /// The list of bots if successful, null otherwise. - private static async Task?> GetTwitchInsightsBots() { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - // Reach out to the api and find out what bots are online. - using var http = new HttpClient(); - HttpResponseMessage response = await http.GetAsync("https://api.twitchinsights.net/v1/bots/all"); - if (!response.IsSuccessStatusCode) { - return null; - } - - byte[]? content = await response.Content.ReadAsByteArrayAsync(); - string? jsonString = Encoding.UTF8.GetString(content); - var liveBotsResponse = JsonConvert.DeserializeObject(jsonString); - if (null == liveBotsResponse) { - return null; - } - - ImmutableHashSet allBots = liveBotsResponse.bots - .Where(s => !string.IsNullOrWhiteSpace(s[0].ToString())) -#pragma warning disable 8602 - .Select(s => s[0].ToString().ToLowerInvariant()) -#pragma warning restore 8602 - .ToImmutableHashSet(); - - foreach (List? list in liveBotsResponse.bots) { - list.Clear(); - } - - liveBotsResponse.bots.Clear(); - liveBotsResponse.bots = null; - liveBotsResponse = null; - content = null; - jsonString = null; - return allBots; - } - - /// - /// Gets the list of all known bots. - /// - /// The list of bots if successful, null otherwise. - private static async Task?> GetCommanderRootBots() { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - // Reach out to the api and find out what bots are online. - using var http = new HttpClient(); - using HttpResponseMessage response = - await http.GetAsync("https://twitch-tools.rootonline.de/blocklist_manager.php?preset=known_bot_users"); - if (!response.IsSuccessStatusCode) { - return null; - } - - byte[]? content = await response.Content.ReadAsByteArrayAsync(); - string? jsonString = Encoding.UTF8.GetString(content); - var liveBotsResponse = JsonConvert.DeserializeObject>(jsonString); - if (null == liveBotsResponse) { - return null; - } - - ImmutableHashSet allBots = liveBotsResponse -#pragma warning disable 8602 - .Select(s => s.ToLowerInvariant()) -#pragma warning restore 8602 - .ToImmutableHashSet(); - - liveBotsResponse.Clear(); - liveBotsResponse = null; - liveBotsResponse = null; - content = null; - jsonString = null; - return allBots; - } -} \ No newline at end of file +// using System.Collections.Immutable; +// using System.Diagnostics; +// using System.Text; +// +// using Newtonsoft.Json; +// +// using Nullinside.Api.Common.Twitch; +// using Nullinside.Api.Model; +// using Nullinside.Api.Model.Ddl; +// using Nullinside.Api.TwitchBot.Model; +// +// using TwitchLib.Api.Helix.Models.Chat.GetChatters; +// +// using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig; +// +// namespace Nullinside.Api.TwitchBot.Bots; +// +// /// +// /// Searches for and bans known bots when they enter the chat. +// /// +// public class BanKnownBots : ABotRule { +// /// +// /// Handles refreshing the collection. +// /// +// private static Task _ = Task.Run(async () => { +// while (true) { +// Task?> twitchInsights = GetTwitchInsightsBots(); +// Task?> commanderRoot = GetCommanderRootBots(); +// await Task.WhenAll(twitchInsights, commanderRoot); +// if (null != twitchInsights.Result) { +// KnownBotListUsername = twitchInsights.Result; +// } +// +// if (null != commanderRoot.Result) { +// KnownBotListUserId = commanderRoot.Result; +// } +// +// // The list is enormous, force garbage collection. +// GC.Collect(); +// GC.WaitForPendingFinalizers(); +// await Task.Delay(TimeSpan.FromMinutes(10)); +// } +// }); +// +// /// +// /// The cache of known bots. +// /// +// public static ImmutableHashSet? KnownBotListUsername { get; set; } +// +// /// +// /// The cache of known bots. +// /// +// public static ImmutableHashSet? KnownBotListUserId { get; set; } +// +// /// +// /// Determine if the rule is enabled in the configuration. +// /// +// /// The user's configuration. +// /// True if it should run, false otherwise. +// public override bool ShouldRun(TwitchUserConfig config) { +// return config is { Enabled: true, BanKnownBots: true }; +// } +// +// /// +// /// Searches for and bans known bots when they enter the chat. +// /// +// /// The user. +// /// The user's configuration. +// /// The twitch api authenticated as the bot user. +// /// The database. +// /// The cancellation token. +// public override async Task Handle(User user, TwitchUserConfig config, ITwitchApiProxy botProxy, +// INullinsideContext db, CancellationToken stoppingToken = new()) { +// if (null == user.TwitchId) { +// return; +// } +// +// // TODO: SKIP MODS AND VIPS +// +// // Get the list of people in the chat. +// List? chatters = +// (await botProxy.GetChannelUsers(user.TwitchId, Constants.BotId, stoppingToken))?.ToList(); +// if (null == chatters || chatters.Count == 0) { +// return; +// } +// +// // Perform the comparison in the lock to prevent multithreading issues. +// // The collection is extremely large so we do not want to make a copy of it. +// var botsInChatInsights = new List(); +// var botsInChatCommanderRoot = new List(); +// ImmutableHashSet? knownBotUsernames = KnownBotListUsername; +// ImmutableHashSet? knownBotUserIds = KnownBotListUserId; +// +// if (null != knownBotUsernames) { +// botsInChatInsights.AddRange(chatters.Where(k => +// knownBotUsernames.Contains(k.UserLogin.ToLowerInvariant()))); +// } +// +// if (null != knownBotUserIds) { +// botsInChatCommanderRoot.AddRange(chatters.Where(k => +// knownBotUserIds.Contains(k.UserId.ToLowerInvariant()))); +// } +// +// // Remove the whitelisted bots +// botsInChatInsights = botsInChatInsights +// .Where(b => !Constants.WhitelistedBots.Contains(b.UserLogin.ToLowerInvariant())).ToList(); +// botsInChatCommanderRoot = botsInChatCommanderRoot +// .Where(b => !Constants.WhitelistedBots.Contains(b.UserLogin.ToLowerInvariant())).ToList(); +// +// // Ban them. +// if (botsInChatInsights.Count != 0) { +// await BanOnce(botProxy, db, user.TwitchId, botsInChatInsights.Select(b => (Id: b.UserId, Username: b.UserLogin)), +// "[Bot] Username on Known Bot List", stoppingToken); +// } +// +// if (botsInChatCommanderRoot.Count != 0) { +// await BanOnce(botProxy, db, user.TwitchId, +// botsInChatCommanderRoot.Select(b => (Id: b.UserId, Username: b.UserLogin)), +// "[Bot] Username on Known Bot List", stoppingToken); +// } +// } +// +// /// +// /// Gets the list of all known bots. +// /// +// /// The list of bots if successful, null otherwise. +// private static async Task?> GetTwitchInsightsBots() { +// var stopwatch = new Stopwatch(); +// stopwatch.Start(); +// // Reach out to the api and find out what bots are online. +// using var http = new HttpClient(); +// HttpResponseMessage response = await http.GetAsync("https://api.twitchinsights.net/v1/bots/all"); +// if (!response.IsSuccessStatusCode) { +// return null; +// } +// +// byte[]? content = await response.Content.ReadAsByteArrayAsync(); +// string? jsonString = Encoding.UTF8.GetString(content); +// var liveBotsResponse = JsonConvert.DeserializeObject(jsonString); +// if (null == liveBotsResponse) { +// return null; +// } +// +// ImmutableHashSet allBots = liveBotsResponse.bots +// .Where(s => !string.IsNullOrWhiteSpace(s[0].ToString())) +// #pragma warning disable 8602 +// .Select(s => s[0].ToString().ToLowerInvariant()) +// #pragma warning restore 8602 +// .ToImmutableHashSet(); +// +// foreach (List? list in liveBotsResponse.bots) { +// list.Clear(); +// } +// +// liveBotsResponse.bots.Clear(); +// liveBotsResponse.bots = null; +// liveBotsResponse = null; +// content = null; +// jsonString = null; +// return allBots; +// } +// +// /// +// /// Gets the list of all known bots. +// /// +// /// The list of bots if successful, null otherwise. +// private static async Task?> GetCommanderRootBots() { +// var stopwatch = new Stopwatch(); +// stopwatch.Start(); +// // Reach out to the api and find out what bots are online. +// using var http = new HttpClient(); +// using HttpResponseMessage response = +// await http.GetAsync("https://twitch-tools.rootonline.de/blocklist_manager.php?preset=known_bot_users"); +// if (!response.IsSuccessStatusCode) { +// return null; +// } +// +// byte[]? content = await response.Content.ReadAsByteArrayAsync(); +// string? jsonString = Encoding.UTF8.GetString(content); +// var liveBotsResponse = JsonConvert.DeserializeObject>(jsonString); +// if (null == liveBotsResponse) { +// return null; +// } +// +// ImmutableHashSet allBots = liveBotsResponse +// #pragma warning disable 8602 +// .Select(s => s.ToLowerInvariant()) +// #pragma warning restore 8602 +// .ToImmutableHashSet(); +// +// liveBotsResponse.Clear(); +// liveBotsResponse = null; +// liveBotsResponse = null; +// content = null; +// jsonString = null; +// return allBots; +// } +// } \ No newline at end of file From b85a5640d3945a2f894f012db41597307b79e0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Sun, 2 Feb 2025 16:37:10 -0500 Subject: [PATCH 3/3] fix: race condition when setting bot token and ignoring banned users There is still another race condition but this should severly limit the surface area of the issue. We need to centralize where the bot token is refreshed instead of allowing 2 separate threads to compete for it. We always wanted to ignore banned users but I forget to exclude them from the mod list. --- .../Services/MainService.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Nullinside.Api.TwitchBot/Services/MainService.cs b/src/Nullinside.Api.TwitchBot/Services/MainService.cs index 18fdbff..214cc23 100644 --- a/src/Nullinside.Api.TwitchBot/Services/MainService.cs +++ b/src/Nullinside.Api.TwitchBot/Services/MainService.cs @@ -152,6 +152,13 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) { if (null == usersWithBotEnabled) { continue; } + + // Get the list of users that are banned. + List? bannedUsers = await GetBannedUsers(db, stoppingToken); + if (null == bannedUsers) { + bannedUsers = Enumerable.Empty().ToList(); + } + var bannedUserIds = bannedUsers.Select(u => u.TwitchId).ToList(); // Get the bot user's information. User? botUser = await db.Users.AsNoTracking() @@ -173,6 +180,8 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) { // Trim channels we aren't a mod in IEnumerable moddedChannels = await botApi.GetUserModChannels(Constants.BotId); + moddedChannels = moddedChannels.Where(m => !bannedUserIds.Contains(m.broadcaster_id)).ToList(); + usersWithBotEnabled = usersWithBotEnabled .Where(u => moddedChannels .Select(m => m.broadcaster_id) @@ -281,6 +290,23 @@ orderby user.TwitchLastScanned .AsNoTracking() .ToListAsync(stoppingToken); } + + /// + /// Retrieve all users that are banned from using the bot. + /// + /// The database. + /// The stopping token. + /// The list of users with the bot enabled. + private async Task?> GetBannedUsers(INullinsideContext db, CancellationToken stoppingToken) { + return await + (from user in db.Users + orderby user.TwitchLastScanned + where user.TwitchId != Constants.BotId && + user.IsBanned + select user) + .AsNoTracking() + .ToListAsync(stoppingToken); + } /// /// Performs the scan on a user. @@ -299,8 +325,8 @@ private async Task DoScan(User user, User botUser, CancellationToken stoppingTok using (IServiceScope scope = _serviceScopeFactory.CreateAsyncScope()) { await using (var db = scope.ServiceProvider.GetRequiredService()) { // Get the API - ITwitchApiProxy? botApi = await db.ConfigureApiAndRefreshToken(botUser, this._api, stoppingToken); - if (null == _botRules || null == user.TwitchConfig || null == botApi) { + this._api.Configure(botUser); + if (null == _botRules || null == user.TwitchConfig) { return; } @@ -308,7 +334,7 @@ private async Task DoScan(User user, User botUser, CancellationToken stoppingTok foreach (IBotRule rule in _botRules) { try { if (rule.ShouldRun(user.TwitchConfig)) { - await rule.Handle(user, user.TwitchConfig, botApi, db, stoppingToken); + await rule.Handle(user, user.TwitchConfig, this._api, db, stoppingToken); } } catch (Exception e) {