diff --git a/src/Nullinside.Api.TwitchBot.Tests/ChatRules/PeakPyTests.cs b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/PeakPyTests.cs
new file mode 100644
index 0000000..67e9ddf
--- /dev/null
+++ b/src/Nullinside.Api.TwitchBot.Tests/ChatRules/PeakPyTests.cs
@@ -0,0 +1,31 @@
+using Moq;
+
+using Nullinside.Api.Common.Twitch;
+using Nullinside.Api.TwitchBot.ChatRules;
+using Nullinside.Api.TwitchBot.Model;
+
+namespace Nullinside.Api.TwitchBot.Tests.ChatRules;
+
+///
+/// Tests the class.
+///
+public class PeakPyTests : AChatRuleUnitTestBase {
+ ///
+ /// Tests the strings typed in chats.
+ ///
+ /// The string that should fail.
+ [Test]
+ [TestCase("@blackysing Sup, your stream could use some hype PeakPy .com fills it quick (remove the space) @4KB,")]
+ [TestCase("@ethuins Yo dude, saw your stream's kinda dead rn PeakPy .com can pump some real eyes in there quick (remove the space) @cMr,")]
+ [TestCase("@floridean Sup, your stream could use some hype PeakPy .com can pump some real followers in there quick (remove the space) @sR4n35hG,")]
+ [TestCase("@floridean Yo, chat's quiet, PeakPy .com brings real viewers instantly (remove the space) @sN3ZZtgF,")]
+ public async Task TestKnownStrings(string badString) {
+ var rule = new PeakPy();
+ var botProxy = new Mock();
+ var chat = new TwitchChatMessage(true, badString, "123", "456");
+
+ // Process the message and assert that we fail the message.
+ bool result = await rule.Handle("123", botProxy.Object, chat, _db).ConfigureAwait(false);
+ Assert.That(result, Is.False);
+ }
+}
\ No newline at end of file
diff --git a/src/Nullinside.Api.TwitchBot/ChatRules/PeakPy.cs b/src/Nullinside.Api.TwitchBot/ChatRules/PeakPy.cs
new file mode 100644
index 0000000..574c98e
--- /dev/null
+++ b/src/Nullinside.Api.TwitchBot/ChatRules/PeakPy.cs
@@ -0,0 +1,34 @@
+using Nullinside.Api.Common.Twitch;
+using Nullinside.Api.Model;
+using Nullinside.Api.TwitchBot.Model;
+
+using TwitchUserConfig = Nullinside.Api.Model.Ddl.TwitchUserConfig;
+
+namespace Nullinside.Api.TwitchBot.ChatRules;
+
+///
+/// Handles "PeakPy" spam.
+///
+public class PeakPy : AChatRule {
+ ///
+ public override bool ShouldRun(TwitchUserConfig config) {
+ return config is { Enabled: true, BanKnownBots: true };
+ }
+
+ ///
+ public override async Task Handle(string channelId, ITwitchApiProxy botProxy, TwitchChatMessage message,
+ INullinsideContext db, CancellationToken stoppingToken = new()) {
+ // The number of spaces per message may chance, so normalize that and lowercase it for comparison.
+ string normalized = string.Concat(message.Message.Split(" ").Where(s => !string.IsNullOrWhiteSpace(s)))
+ .ToLowerInvariant();
+
+ // Message will start with any of these variations.
+ if (message.IsFirstMessage && normalized.Contains("peakpy")) {
+ await BanAndLog(channelId, botProxy, new[] { (message.UserId, message.Username) },
+ "[Bot] Spam (PeakPy)", db, stoppingToken).ConfigureAwait(false);
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file