Completely untested and totally experimental rate limit feature

This commit is contained in:
barelyprofessional
2025-09-08 15:09:59 -05:00
parent f9445d407a
commit ff1d83d9f7
18 changed files with 420 additions and 28 deletions

View File

@@ -20,6 +20,8 @@ public class SetRoleCommand : ICommand
public string? HelpText => "Set a user's role";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
@@ -47,6 +49,7 @@ public class CacheClearAdminCommand : ICommand
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
@@ -69,6 +72,7 @@ public class NewKickChannelCommand : ICommand
public string? HelpText => "Add a Kick channel to the bot's database";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var autoCapture = false;
@@ -115,6 +119,7 @@ public class RemoveStreamChannelCommand : ICommand
public string? HelpText => "Remove a Kick channel from the bot's database";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
@@ -140,6 +145,7 @@ public class ReconnectKickCommand : ICommand
public string? HelpText => "Disconnect from Kick so the watchdog can reconnect it";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
if (botInstance.BotServices.KickClient == null)
@@ -163,6 +169,7 @@ public class NewPartiChannelCommand : ICommand
public string? HelpText => "Add a Parti channel to the bot's database";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var autoCapture = false;
@@ -210,6 +217,7 @@ public class NewDLiveChannelCommand : ICommand
public string? HelpText => "Add a DLive channel to the bot's database";
public UserRight RequiredRight => UserRight.Admin;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var autoCapture = false;
@@ -251,6 +259,7 @@ public class AddCourtHearingCommand : ICommand
public string? HelpText => "Add a court hearing to the bot's calendar";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var hearings = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotCourtCalendar)).JsonDeserialize<List<CourtHearingModel>>();
@@ -281,6 +290,7 @@ public class RemoveCourtHearingCommand : ICommand
public string? HelpText => "Remove a hearing from the bot's calendar";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var hearings = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotCourtCalendar)).JsonDeserialize<List<CourtHearingModel>>();
@@ -312,7 +322,7 @@ public class DeleteMessagesCommand : ICommand
public string? HelpText => "Delete the most recent x number of messages";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -345,7 +355,7 @@ public class IgnoreCommand : ICommand
public string? HelpText => "Ignore a user by ID";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -379,7 +389,7 @@ public class UnignoreCommand : ICommand
public string? HelpText => "Unignore a user by ID";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -413,7 +423,7 @@ public class SetAlmanacTextCommand : ICommand
public string? HelpText => "Set the almanac text to whatever";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -431,7 +441,7 @@ public class SetAlmanacIntervalCommand : ICommand
public string? HelpText => "Set the almanac interval to whatever in seconds";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -461,7 +471,7 @@ public class StopAlmanacCommand : ICommand
public string? HelpText => "Stop the almanac reminder";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -490,7 +500,7 @@ public class StartAlmanacCommand : ICommand
public string? HelpText => "Start the almanac reminder";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Humanizer;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
@@ -15,6 +16,7 @@ public class HowlggStatsCommand : ICommand
public string? HelpText => "Get betting statistics in the given window";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var window = Convert.ToInt32(arguments["window"].Value);
@@ -42,6 +44,7 @@ public class HowlggRecentBetCommand : ICommand
public string? HelpText => "Get the most recent 3 bets";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings = await SettingsProvider.GetMultipleValuesAsync([

View File

@@ -1,16 +1,18 @@
using System.Text.RegularExpressions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetWsClient.Models.Events;
namespace KfChatDotNetBot.Commands;
internal interface ICommand
public interface ICommand
{
List<Regex> Patterns { get; }
// Set to null to disable help for a given command
string? HelpText { get; }
UserRight RequiredRight { get; }
TimeSpan Timeout { get; }
RateLimitOptionsModel? RateLimitOptions { get; }
Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx);
}

View File

@@ -21,7 +21,7 @@ public class AddImageCommand : ICommand
public string? HelpText => "Add an image to the image rotation specified";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -61,7 +61,7 @@ public class RemoveImageCommand : ICommand
public string? HelpText => "Remove an image from the image rotation specified";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -99,7 +99,7 @@ public class ListImageCommand : ICommand
public string? HelpText => "Remove an image from the image rotation specified";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -138,7 +138,12 @@ public class GetRandomImage : ICommand
public string? HelpText => "Get a random image";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromMinutes(10);
public RateLimitOptionsModel? RateLimitOptions => new()
{
Window = TimeSpan.FromSeconds(10),
MaxInvocations = 3,
Flags = RateLimitFlags.NoResponse | RateLimitFlags.UseEntireMessage
};
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{

View File

@@ -16,6 +16,7 @@ public class JuiceCommand : ICommand
public bool HideFromHelp => false;
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await using var db = new ApplicationDbContext();
@@ -81,6 +82,7 @@ public class JuiceStatsCommand : ICommand
public bool HideFromHelp => false;
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
int top;

View File

@@ -2,6 +2,7 @@
using Humanizer;
using Humanizer.Localisation;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings;
@@ -20,7 +21,7 @@ public class GetBalanceCommand : ICommand
public string? HelpText => "Get your gamba balance";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -39,7 +40,7 @@ public class GetExclusionCommand : ICommand
public string? HelpText => "Get your exclusion status";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -72,7 +73,7 @@ public class SendJuiceCommand : ICommand
public string? HelpText => "Send juice to somebody";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -118,7 +119,12 @@ public class RakebackCommand : ICommand
public string? HelpText => "Collect your rakeback";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel
{
MaxInvocations = 1,
Window = TimeSpan.FromSeconds(30),
Flags = RateLimitFlags.AutoDeleteCooldownResponse
};
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -167,7 +173,12 @@ public class LossbackCommand : ICommand
public string? HelpText => "Collect your lossback";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel
{
Window = TimeSpan.FromSeconds(30),
MaxInvocations = 1,
Flags = RateLimitFlags.AutoDeleteCooldownResponse
};
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -213,12 +224,11 @@ public class AbandonKasinoCommand : ICommand
public List<Regex> Patterns => [
new Regex(@"^abandon$", RegexOptions.IgnoreCase),
new Regex(@"^abandon confirm$", RegexOptions.IgnoreCase)
];
public string? HelpText => "Abandon your Keno Kasino gambler account";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{

View File

@@ -16,6 +16,7 @@ public class InsanityCommand : ICommand
public string? HelpText => "Insanity";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
// ReSharper disable once StringLiteralTypo
@@ -29,6 +30,7 @@ public class TwistedCommand : ICommand
public string? HelpText => "Get it twisted";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
// ReSharper disable once StringLiteralTypo
@@ -45,6 +47,7 @@ public class CrackedCommand : ICommand
public string? HelpText => "Crackhead Zalgo text";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var logger = LogManager.GetCurrentClassLogger();
@@ -67,6 +70,7 @@ public class CleanCommand : ICommand
public string? HelpText => "How long has Bossman been clean?";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings =
@@ -90,6 +94,7 @@ public class RehabCommand : ICommand
public string? HelpText => "How long until rehab is over?";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings =
@@ -121,6 +126,7 @@ public class NextPoVisitCommand : ICommand
public string? HelpText => "How long until the next PO visit?";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(120);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var time = await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotPoNextVisit);
@@ -160,6 +166,7 @@ public class NextCourtHearingCommand : ICommand
public string? HelpText => "How long until the next court hearing?";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(120);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var hearings = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotCourtCalendar)).JsonDeserialize<List<CourtHearingModel>>();
@@ -215,6 +222,7 @@ public class JailCommand : ICommand
public string? HelpText => "How long has Bossman been in jail?";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings = await SettingsProvider.GetMultipleValuesAsync([BuiltIn.Keys.BotJailStartTime, BuiltIn.Keys.TwitchBossmanJackUsername]);
@@ -235,6 +243,7 @@ public class LastStreamCommand : ICommand
public string? HelpText => "How long ago did Austin Gambles last stream (on Twitch)?";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings = await SettingsProvider.GetMultipleValuesAsync([
@@ -270,6 +279,7 @@ public class AlmanacCommand : ICommand
public string? HelpText => "Return details on how to submit almanac entries";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var text = await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotAlmanacText);

View File

@@ -1,6 +1,7 @@
using System.Text.RegularExpressions;
using Humanizer;
using Humanizer.Localisation;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
@@ -15,6 +16,7 @@ public class MomCommand : ICommand
public bool HideFromHelp => false;
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Humanizer;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
@@ -15,6 +16,7 @@ public class RainbetStatsCommand : ICommand
public string? HelpText => "Get betting statistics in the given window";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var window = Convert.ToInt32(arguments["window"].Value);
@@ -41,6 +43,7 @@ public class RainbetRecentBetCommand : ICommand
public string? HelpText => "Get the most recent 3 bets";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var settings = await SettingsProvider.GetMultipleValuesAsync([

View File

@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
@@ -15,7 +16,7 @@ public class GetRestreamCommand : ICommand
public string? HelpText => "Grab restream URL";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -33,7 +34,7 @@ public class SetRestreamCommand : ICommand
public string? HelpText => "Set restream URL";
public UserRight RequiredRight => UserRight.TrueAndHonest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -51,7 +52,7 @@ public class SelfPromoCommand : ICommand
public string? HelpText => "Promote your shit";
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{
@@ -80,7 +81,7 @@ public class GetRestreamPlainCommand : ICommand
public string? HelpText => "Grab restream URL with plain prefixed";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
CancellationToken ctx)
{

View File

@@ -17,6 +17,7 @@ public class EditTestCommand : ICommand
public UserRight RequiredRight => UserRight.Admin;
// Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var logger = LogManager.GetCurrentClassLogger();
@@ -63,6 +64,7 @@ public class TimeoutTestCommand : ICommand
public UserRight RequiredRight => UserRight.Admin;
// Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await Task.Delay(TimeSpan.FromMinutes(1), ctx);
@@ -79,6 +81,7 @@ public class ExceptionTestCommand : ICommand
public UserRight RequiredRight => UserRight.Admin;
// Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => null;
public Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
throw new Exception("Caused by the test exception command");
@@ -95,6 +98,7 @@ public class LengthLimitTestCommand : ICommand
public UserRight RequiredRight => UserRight.Admin;
// Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var logger = LogManager.GetCurrentClassLogger();
@@ -110,13 +114,34 @@ public class LengthLimitTestCommand : ICommand
await Task.Delay(TimeSpan.FromSeconds(5), ctx);
logger.Info($"niceTruncation => {niceTruncation.Status}; exactTruncation => {exactTruncation.Status}; doNothing => {doNothing.Status}; refuseToSend => {refuseToSend.Status}");
if (niceTruncation.ChatMessageId != null)
botInstance.KfClient.DeleteMessage(niceTruncation.ChatMessageId.Value);
await botInstance.KfClient.DeleteMessageAsync(niceTruncation.ChatMessageId.Value);
if (exactTruncation.ChatMessageId != null)
botInstance.KfClient.DeleteMessage(exactTruncation.ChatMessageId.Value);
await botInstance.KfClient.DeleteMessageAsync(exactTruncation.ChatMessageId.Value);
if (doNothing.ChatMessageId != null)
botInstance.KfClient.DeleteMessage(doNothing.ChatMessageId.Value);
await botInstance.KfClient.DeleteMessageAsync(doNothing.ChatMessageId.Value);
// Should never happen
if (refuseToSend.ChatMessageId != null)
botInstance.KfClient.DeleteMessage(refuseToSend.ChatMessageId.Value);
await botInstance.KfClient.DeleteMessageAsync(refuseToSend.ChatMessageId.Value);
}
}
public class RateLimitTestCommand : ICommand
{
public List<Regex> Patterns => [
new Regex("^test ratelimit$")
];
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Admin;
// Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes
public TimeSpan Timeout => TimeSpan.FromSeconds(15);
public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel
{
MaxInvocations = 1,
Window = TimeSpan.FromSeconds(60)
};
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await botInstance.SendChatMessageAsync("Nigger", true);
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetWsClient.Models.Events;
@@ -7,9 +8,10 @@ namespace KfChatDotNetBot.Commands;
public class TimeCommand : ICommand
{
public List<Regex> Patterns => [new Regex("^time")];
public string? HelpText => "Get current time in AGT";
public string? HelpText => "Get current time in BMT";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var bmt = new DateTimeOffset(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow,

View File

@@ -1,5 +1,6 @@
using System.Reflection;
using System.Text.RegularExpressions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetWsClient.Models.Events;
@@ -14,6 +15,7 @@ public class TempEnableDiscordRelayingCommand : ICommand
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
botInstance.BotServices.TemporarilyBypassGambaSeshForDiscord = true;
@@ -30,6 +32,7 @@ public class TempSuppressGambaMessages : ICommand
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
botInstance.BotServices.TemporarilySuppressGambaMessages = true;
@@ -46,6 +49,7 @@ public class EnableGambaMessages : ICommand
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
botInstance.BotServices.TemporarilySuppressGambaMessages = false;
@@ -62,6 +66,7 @@ public class GetVersionCommand : ICommand
public string? HelpText => null;
public UserRight RequiredRight => UserRight.Loser;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
var version = Assembly.GetEntryAssembly()?

View File

@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetWsClient.Models.Events;
using Microsoft.EntityFrameworkCore;
@@ -15,6 +16,7 @@ public class WhoisCommand : ICommand
public string? HelpText => "Lookup user IDs by username";
public UserRight RequiredRight => UserRight.Guest;
public TimeSpan Timeout => TimeSpan.FromSeconds(10);
public RateLimitOptionsModel? RateLimitOptions => null;
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
{
await using var db = new ApplicationDbContext();

View File

@@ -0,0 +1,83 @@
namespace KfChatDotNetBot.Models;
public class RateLimitBucketEntryModel
{
/// <summary>
/// Database user ID of the user whose entry this belongs to
/// </summary>
public required int UserId { get; set; }
/// <summary>
/// When the entry was created in the bucket
/// </summary>
public required DateTimeOffset EntryCreated { get; set; }
/// <summary>
/// When the entry is expected to expire based on the command's window
/// </summary>
public required DateTimeOffset EntryExpires { get; set; }
/// <summary>
/// String representation of the command using ICommand.GetType().Name
/// </summary>
public required string CommandInvoked { get; set; }
/// <summary>
/// Hashed contents of the message for if UseEntireMessage is enabled
/// </summary>
public required string MessageHash { get; set; }
}
public class RateLimitOptionsModel
{
/// <summary>
/// Window of time to count an invocation towards the rate limit
/// </summary>
public required TimeSpan Window { get; set; }
/// <summary>
/// Maximum number of permitted invocations within the window before triggering the rate limit
/// </summary>
public required int MaxInvocations { get; set; }
/// <summary>
/// Optional set of flags to configure the behavior of the rate limiter
/// </summary>
public RateLimitFlags Flags { get; set; }
}
public class IsRateLimitedModel
{
/// <summary>
/// Is the user's request rate limited?
/// </summary>
public required bool IsRateLimited { get; set; }
/// <summary>
/// When the oldest entry expires so users know when they can next use the command
/// This is set to null if the user is not rate limited
/// </summary>
public DateTimeOffset? OldestEntryExpires { get; set; }
}
[Flags]
public enum RateLimitFlags
{
/// <summary>
/// Silently ignore a user when they trigger a rate limit
/// </summary>
NoResponse,
/// <summary>
/// The default behavior is to rate limit based on command invoked.
/// UseEntireMessage changes it to consider dissimilar messages which invoke
/// the same command as being separate for the purposes of rate limiting.
/// With this, only identical messages count towards the rate limit.
/// </summary>
UseEntireMessage,
/// <summary>
/// The rate limit is global instead of applying per-user
/// </summary>
Global,
/// <summary>
/// Exempt users with a higher than default level from rate limiting
/// </summary>
ExemptPrivilegedUsers,
/// <summary>
/// Automatically clean up the cooldown response sent to a user
/// Mutually exclusive with NoResponse
/// </summary>
AutoDeleteCooldownResponse
}

View File

@@ -1,7 +1,9 @@
using System.Text.RegularExpressions;
using Humanizer;
using Humanizer.Localisation;
using KfChatDotNetBot.Commands;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;
@@ -34,6 +36,8 @@ internal class BotCommands
{
_logger.Debug($"Found command {command.GetType().Name}");
}
_ = CleanupExpiredRateLimitEntriesTask();
}
internal void ProcessMessage(MessageModel message)
@@ -89,12 +93,26 @@ internal class BotCommands
return;
}
}
if (user.UserRight < command.RequiredRight)
{
_bot.SendChatMessage($"@{message.Author.Username}, you do not have access to use this command. Your rank: {user.UserRight.Humanize()}; Required rank: {command.RequiredRight.Humanize()}", true);
if (continueAfterProcess) continue;
break;
}
if (command.RateLimitOptions != null)
{
var isRateLimited = RateLimitService.IsRateLimited(user, command, message.MessageRawHtmlDecoded);
if (isRateLimited.IsRateLimited)
{
_ = SendCooldownResponse(user, command.RateLimitOptions, isRateLimited.OldestEntryExpires!.Value, command.GetType().Name);
}
else
{
RateLimitService.AddEntry(user, command, message.MessageRawHtmlDecoded);
}
}
_ = ProcessMessageAsync(command, message, user, match.Groups);
if (!continueAfterProcess) break;
}
@@ -136,6 +154,48 @@ internal class BotCommands
$"🤑🤑 {user.KfUsername} has leveled up to to {newLevel.VipLevel.Icon} {newLevel.VipLevel.Name} Tier {newLevel.Tier} " +
$"and received a bonus of {await payout.FormatKasinoCurrencyAsync()}", true);
}
private async Task SendCooldownResponse(UserDbModel user, RateLimitOptionsModel options, DateTimeOffset oldestEntryExpires, string commandName)
{
if (options.Flags.HasFlag(RateLimitFlags.NoResponse)) return;
var timeRemaining = oldestEntryExpires - DateTimeOffset.UtcNow;
var message = await _bot.SendChatMessageAsync($"{user.FormatUsername()}, please wait {timeRemaining.Humanize(maxUnit: TimeUnit.Minute, minUnit: TimeUnit.Millisecond, precision: 2)} before attempting to run {commandName} again.", true);
if (!options.Flags.HasFlag(RateLimitFlags.AutoDeleteCooldownResponse)) return;
var i = 0;
while (message.ChatMessageId == null)
{
i++;
await Task.Delay(250, _cancellationToken);
if (i > 30)
{
_logger.Error("Gave up waiting for Sneedchat to give us the message ID for removing a cooldown notification");
return;
}
if (message.Status is SentMessageTrackerStatus.NotSending or SentMessageTrackerStatus.Lost)
{
_logger.Error("Cooldown message was suppressed or lost");
return;
}
}
var autoDeleteInterval =
(await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRateLimitCooldownAutoDeleteDelay)).ToType<int>();
await Task.Delay(autoDeleteInterval, _cancellationToken);
await _bot.KfClient.DeleteMessageAsync(message.ChatMessageId.Value);
}
private async Task CleanupExpiredRateLimitEntriesTask()
{
while (!_cancellationToken.IsCancellationRequested)
{
var interval = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.BotRateLimitExpiredEntryCleanupInterval))
.ToType<int>();
await Task.Delay(TimeSpan.FromSeconds(interval), _cancellationToken);
_logger.Info("Cleaning up expired rate limit entries");
RateLimitService.CleanupExpiredEntries();
}
}
private static bool HasAttribute<T>(ICommand command) where T : Attribute
{

View File

@@ -0,0 +1,149 @@
using System.Runtime.Caching;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using KfChatDotNetBot.Commands;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using NLog;
namespace KfChatDotNetBot.Services;
public static class RateLimitService
{
private static Logger _logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Check whether a user is rate limited for a given command
/// </summary>
/// <param name="user">User you wish to check</param>
/// <param name="command">Command the user is invoking</param>
/// <param name="message">Message the user sent</param>
/// <returns></returns>
public static IsRateLimitedModel IsRateLimited(UserDbModel user, ICommand command, string message)
{
var result = new IsRateLimitedModel
{
IsRateLimited = false
};
if (command.RateLimitOptions == null) return result;
if (command.RateLimitOptions.Flags.HasFlag(RateLimitFlags.ExemptPrivilegedUsers) &&
user.UserRight > UserRight.Guest) return result;
var entries = GetBucketEntries(command.GetType().Name);
if (!command.RateLimitOptions.Flags.HasFlag(RateLimitFlags.Global))
{
entries = entries.Where(x => x.UserId == user.Id).ToList();
}
if (command.RateLimitOptions.Flags.HasFlag(RateLimitFlags.UseEntireMessage))
{
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(message)));
entries = entries.Where(x => x.MessageHash == hash).ToList();
}
var now = DateTimeOffset.UtcNow;
entries = entries.Where(x => x.EntryExpires > now).ToList();
if (entries.Count >= command.RateLimitOptions.MaxInvocations)
{
result.IsRateLimited = true;
result.OldestEntryExpires = entries.OrderBy(x => x.EntryCreated).Last().EntryExpires;
}
return result;
}
/// <summary>
/// Get all the bucket entries for a given command
/// </summary>
/// <param name="commandName">String representation of the command.
/// Get it by running command.GetType().Name</param>
/// <returns>A list of entries</returns>
/// <exception cref="InvalidOperationException">Thrown if the cached entries were somehow null when converted to a string</exception>
public static List<RateLimitBucketEntryModel> GetBucketEntries(string commandName)
{
var cache = MemoryCache.Default;
var entries = cache.Get($"RateLimitBucket:{commandName}");
if (entries == null) return [];
List<RateLimitBucketEntryModel> bucketEntries;
try
{
bucketEntries = JsonSerializer.Deserialize<List<RateLimitBucketEntryModel>>((string)entries) ??
throw new InvalidOperationException();
}
catch (Exception e)
{
_logger.Error($"Caught an exception when trying to deserialize RateLimitBucket entries for {commandName}. JSON follows");
_logger.Error(entries);
_logger.Error("Exception follows");
_logger.Error(e);
return [];
}
return bucketEntries;
}
/// <summary>
/// Save the current state of bucket entries for a given command
/// </summary>
/// <param name="commandName">String representation of the command.
/// Get it by running command.GetType().Name</param>
/// <param name="entries">Entries you wish to save</param>
public static void SaveBucketEntries(string commandName, List<RateLimitBucketEntryModel> entries)
{
var cache = MemoryCache.Default;
cache.Set($"RateLimitBucket:{commandName}", JsonSerializer.Serialize(entries),
new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) });
}
/// <summary>
/// Remove the most recent entry for a given user and command
/// Use this if you want to invalidate an entry as forgiveness for invalid user input
/// </summary>
/// <param name="user">User to remove the entry for</param>
/// <param name="command">Command the user ran</param>
public static void RemoveMostRecentEntry(UserDbModel user, ICommand command)
{
var entries = GetBucketEntries(command.GetType().Name);
var lastEntry = entries.Where(x => x.UserId == user.Id).OrderBy(x => x.EntryCreated).LastOrDefault();
if (lastEntry == null) return;
entries.Remove(lastEntry);
SaveBucketEntries(command.GetType().Name, entries);
}
/// <summary>
/// Add an entry to the rate limit bucket for the given command
/// </summary>
/// <param name="user">User the entry belongs to</param>
/// <param name="command">Command the user ran</param>
/// <param name="message">The user's message</param>
public static void AddEntry(UserDbModel user, ICommand command, string message)
{
if (command.RateLimitOptions == null) return;
var commandName = command.GetType().Name;
var entries = GetBucketEntries(commandName);
entries.Add(new RateLimitBucketEntryModel
{
UserId = user.Id,
EntryCreated = DateTimeOffset.UtcNow,
EntryExpires = DateTimeOffset.UtcNow + command.RateLimitOptions.Window,
CommandInvoked = commandName,
MessageHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(message)))
});
SaveBucketEntries(commandName, entries);
}
/// <summary>
/// Removes entries which have expired for all commands in the rate limit bucket
/// </summary>
public static void CleanupExpiredEntries()
{
var cache = MemoryCache.Default;
var now = DateTimeOffset.UtcNow;
foreach (var entry in cache.Select(kvp => kvp.Key).Where(kvp => kvp.StartsWith("RateLimitBucket:")).ToList().OfType<string>())
{
_logger.Info($"Cleaning up expired entries for {entry}");
var commandName = entry.Replace("RateLimitBucket:", string.Empty);
var entries = GetBucketEntries(commandName);
SaveBucketEntries(commandName, entries.Where(x => x.EntryExpires > now).ToList());
}
}
}

View File

@@ -1038,6 +1038,22 @@ public static class BuiltIn
Default = "7500",
ValueType = SettingValueType.Text,
Regex = WholeNumberRegex
},
new BuiltInSettingsModel
{
Key = Keys.BotRateLimitCooldownAutoDeleteDelay,
Description = "Delay in milliseconds before removing a cooldown message set to auto delete",
Default = "15000",
ValueType = SettingValueType.Text,
Regex = WholeNumberRegex
},
new BuiltInSettingsModel
{
Key = Keys.BotRateLimitExpiredEntryCleanupInterval,
Description = "How often to cleanup expired rate limit entries in seconds",
Default = "300",
ValueType = SettingValueType.Text,
Regex = WholeNumberRegex
}
];
@@ -1157,5 +1173,7 @@ public static class BuiltIn
public static string MoneyLossbackMinimumAmount = "Money.Lossback.MinimumAmount";
public static string BotImageChinkSelfDestruct = "Bot.Image.ChinkSelfDestruct";
public static string BotImageChinkSelfDestructDelay = "Bot.Image.ChinkSelfDestructDelay";
public static string BotRateLimitCooldownAutoDeleteDelay = "Bot.RateLimit.CooldownAutoDeleteDelay";
public static string BotRateLimitExpiredEntryCleanupInterval = "Bot.RateLimit.ExpiredEntryCleanupInterval";
}
}