Basic leaky bucket timeout

This commit is contained in:
Nikolay Kochulin 2019-12-07 13:38:07 +00:00
parent 29af7c9183
commit 2b17d3925a
15 changed files with 199 additions and 54 deletions

View File

@ -23,18 +23,9 @@ namespace JetKarmaBot
Me = await Client.GetMeAsync();
}
public Task<bool> Execute(object sender, MessageEventArgs args)
public Task<bool> Execute(CommandString cmd, MessageEventArgs args)
{
log.Debug("Message received");
var text = args.Message.Text;
if (CommandString.TryParse(text, out var cmd))
{
if (cmd.UserName != null && cmd.UserName != Me.Username)
{
// directed not at us!
log.Debug("Message not directed at us");
return Task.FromResult(false);
}
try
{
@ -49,7 +40,6 @@ namespace JetKarmaBot
log.Error($"Error while handling command {cmd.Command}!");
log.Error(e);
}
}
return Task.FromResult(false);
}

View File

@ -15,8 +15,8 @@ namespace JetKarmaBot.Commands
class AwardCommand : IChatCommand
{
public IReadOnlyCollection<string> Names => new[] { "award", "revoke" };
[Inject]
private Logger log;
[Inject] private Logger log;
[Inject] private TimeoutManager Timeout;
public async Task<bool> Execute(CommandString cmd, MessageEventArgs args)
{
@ -24,6 +24,7 @@ namespace JetKarmaBot.Commands
{
var currentLocale = Locale[(await db.Chats.FindAsync(args.Message.Chat.Id)).Locale];
var awarder = args.Message.From;
string awardTypeText = null;
int recipientId = default(int);
foreach (string arg in cmd.Parameters)
@ -33,12 +34,14 @@ namespace JetKarmaBot.Commands
if (recipientId != default(int))
{
await Client.SendTextMessageAsync(args.Message.Chat.Id, currentLocale["jetkarmabot.award.errdup"]);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
recipientId = await db.Users.Where(x => x.Username == arg).Select(x => x.UserId).FirstOrDefaultAsync();
if (recipientId == default(int))
{
await Client.SendTextMessageAsync(args.Message.Chat.Id, currentLocale["jetkarmabot.award.errbadusername"]);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
}
@ -49,6 +52,7 @@ namespace JetKarmaBot.Commands
else
{
await Client.SendTextMessageAsync(args.Message.Chat.Id, currentLocale["jetkarmabot.award.errdup"]);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
}
@ -62,10 +66,10 @@ namespace JetKarmaBot.Commands
if (recipientId == default(int))
{
await Client.SendTextMessageAsync(args.Message.Chat.Id, currentLocale["jetkarmabot.award.errawardnoreply"]);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
var awarder = args.Message.From;
bool awarding = cmd.Command == "award";
@ -75,6 +79,7 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
currentLocale["jetkarmabot.award.errawardself"],
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
@ -86,6 +91,7 @@ namespace JetKarmaBot.Commands
? currentLocale["jetkarmabot.award.errawardbot"]
: currentLocale["jetkarmabot.award.errrevokebot"],
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("AwardFailure", awarder.Id, db);
return true;
}
@ -93,15 +99,6 @@ namespace JetKarmaBot.Commands
global::JetKarmaBot.Models.AwardType awardType = awardTypeText != null
? await db.AwardTypes.FirstAsync(at => at.CommandName == awardTypeText)
: await db.AwardTypes.FindAsync((sbyte)1);
DateTime cutoff = DateTime.Now - TimeSpan.FromMinutes(5);
if (await db.Awards.Where(x => x.Date > cutoff && x.FromId == awarder.Id).CountAsync() >= 10)
{
await Client.SendTextMessageAsync(
args.Message.Chat.Id,
currentLocale["jetkarmabot.award.ratelimit"],
replyToMessageId: args.Message.MessageId);
return true;
}
await db.Awards.AddAsync(new Models.Award()
{
AwardTypeId = awardType.AwardTypeId,
@ -130,6 +127,7 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
response,
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("AwardSuccess", awarder.Id, db);
return true;
}
}

View File

@ -12,8 +12,8 @@ namespace JetKarmaBot.Commands
class LocaleCommand : IChatCommand
{
public IReadOnlyCollection<string> Names => new[] { "changelocale", "locale" };
[Inject]
private Logger log;
[Inject] private Logger log;
[Inject] private TimeoutManager Timeout;
public async Task<bool> Execute(CommandString cmd, MessageEventArgs args)
{
@ -26,6 +26,7 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
currentLocale["jetkarmabot.changelocale.getlocale"],
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("LocaleFailure", args.Message.From.Id, db);
return true;
}
else if (cmd.Parameters[0] == "list")
@ -35,6 +36,7 @@ namespace JetKarmaBot.Commands
currentLocale["jetkarmabot.changelocale.listalltext"] + "\n"
+ string.Join("\n", Locale.Select(a => a.Key)),
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("LocaleFailure", args.Message.From.Id, db);
return true;
}
else if (cmd.Parameters[0] == "all")
@ -43,6 +45,7 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
currentLocale["jetkarmabot.changelocale.errorall"],
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("LocaleFailure", args.Message.From.Id, db);
return true;
}
string localeId;
@ -59,6 +62,7 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
currentLocale["jetkarmabot.changelocale.toomany"] + "\n" + string.Join("\n", (e.Data["LocaleNames"] as Locale[]).Select(x => x.Name)),
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("LocaleFailure", args.Message.From.Id, db);
return true;
}
(await db.Chats.FindAsync(args.Message.Chat.Id)).Locale = localeId;
@ -72,6 +76,7 @@ namespace JetKarmaBot.Commands
(currentLocale.HasNote ? currentLocale["jetkarmabot.changelocale.beforenote"] + currentLocale.Note + "\n" : "")
+ currentLocale["jetkarmabot.changelocale.justchanged"],
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("LocaleSuccess", args.Message.From.Id, db);
return true;
}
}

View File

@ -15,6 +15,7 @@ namespace JetKarmaBot.Commands
[Inject] KarmaContextFactory Db;
[Inject] TelegramBotClient Client { get; set; }
[Inject] Localization Locale { get; set; }
[Inject] TimeoutManager Timeout { get; set; }
public IReadOnlyCollection<string> Names => new[] { "currencies", "awardtypes" };
public string Description => "Shows all award types";
@ -35,6 +36,7 @@ namespace JetKarmaBot.Commands
resp,
replyToMessageId: args.Message.MessageId,
parseMode: ParseMode.Html);
await Timeout.ApplyCost("Currencies", args.Message.From.Id, db);
return true;
}
}

View File

@ -13,6 +13,7 @@ namespace JetKarmaBot.Commands
[Inject] KarmaContextFactory Db;
[Inject] TelegramBotClient Client { get; set; }
[Inject] Localization Locale { get; set; }
[Inject] TimeoutManager Timeout { get; set; }
[Inject] ChatCommandRouter Router;
public IReadOnlyCollection<string> Names => new[] { "help" };
@ -34,6 +35,7 @@ namespace JetKarmaBot.Commands
using (var db = Db.GetContext())
{
var currentLocale = Locale[(await db.Chats.FindAsync(args.Message.Chat.Id)).Locale];
await Timeout.ApplyCost("Help", args.Message.From.Id, db);
if (cmd.Parameters.Length < 1)
{
await Client.SendTextMessageAsync(

View File

@ -51,12 +51,14 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
response,
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("Leaderboard", args.Message.From.Id, db);
return true;
}
}
[Inject] KarmaContextFactory Db { get; set; }
[Inject] TelegramBotClient Client { get; set; }
[Inject] TimeoutManager Timeout { get; set; }
[Inject] Localization Locale { get; set; }
public string Description => "Shows the people with the most of a specific award.";

View File

@ -71,12 +71,14 @@ namespace JetKarmaBot.Commands
args.Message.Chat.Id,
response,
replyToMessageId: args.Message.MessageId);
await Timeout.ApplyCost("Status", args.Message.From.Id, db);
return true;
}
}
[Inject] KarmaContextFactory Db { get; set; }
[Inject] TelegramBotClient Client { get; set; }
[Inject] TimeoutManager Timeout { get; set; }
[Inject] Localization Locale { get; set; }
public string Description => "Shows the amount of awards that you have";

View File

@ -2,6 +2,7 @@
using Newtonsoft.Json;
using JsonNet.PrivateSettersContractResolvers;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace JetKarmaBot
{
@ -21,7 +22,19 @@ namespace JetKarmaBot
}
public ProxySettings Proxy { get; private set; }
public bool SqlDebug { get; private set; }
public class TimeoutConfig
{
public int DebtLimitSeconds { get; private set; } = 60 * 60 * 2;
public Dictionary<string, int> CommandCostsSeconds { get; private set; } = new Dictionary<string, int>()
{
{"AwardSuccessful", 60*15},
{"AwardFailed", 60*5},
{"Default", 60*5}
};
public int SaveIntervalSeconds { get; private set; } = 60 * 5;
}
public TimeoutConfig Timeout { get; private set; } = new TimeoutConfig();
public bool SqlDebug { get; private set; } = false;
}
public abstract class ConfigBase

View File

@ -5,6 +5,7 @@ using Perfusion;
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot;
@ -19,9 +20,13 @@ namespace JetKarmaBot
[Inject] Config Config { get; set; }
[Inject] IContainer Container { get; set; }
[Inject] KarmaContextFactory Db { get; set; }
[Inject] TimeoutManager Timeout { get; set; }
[Inject] Localization Locale { get; set; }
TelegramBotClient Client { get; set; }
ChatCommandRouter Commands;
Task timeoutWaitTask;
CancellationTokenSource timeoutWaitTaskToken;
public async Task Init()
{
@ -35,6 +40,9 @@ namespace JetKarmaBot
Client = new TelegramBotClient(Config.ApiKey, httpProxy);
Container.AddInstance(Client);
timeoutWaitTaskToken = new CancellationTokenSource();
timeoutWaitTask = Timeout.SaveLoop(timeoutWaitTaskToken.Token);
await InitCommands(Container);
Client.OnMessage += BotOnMessageReceived;
@ -43,29 +51,59 @@ namespace JetKarmaBot
public async Task Stop()
{
Client.StopReceiving();
timeoutWaitTaskToken.Cancel();
try
{
await timeoutWaitTask;
}
catch (OperationCanceledException) { }
await Timeout.Save();
Dispose();
}
#region service
void BotOnMessageReceived(object sender, MessageEventArgs messageEventArgs)
void BotOnMessageReceived(object sender, MessageEventArgs args)
{
var message = messageEventArgs.Message;
var message = args.Message;
if (message == null || message.Type != MessageType.Text)
return;
if (!CommandString.TryParse(args.Message.Text, out var cmd))
return;
if (cmd.UserName != null && cmd.UserName != Commands.Me.Username)
return;
Task.Run(async () =>
{
using (KarmaContext db = Db.GetContext())
{
await AddUserToDatabase(db, messageEventArgs.Message.From);
if (messageEventArgs.Message.ReplyToMessage != null)
await AddUserToDatabase(db, messageEventArgs.Message.ReplyToMessage.From);
if (!db.Chats.Any(x => x.ChatId == messageEventArgs.Message.Chat.Id))
db.Chats.Add(new Models.Chat { ChatId = messageEventArgs.Message.Chat.Id });
await AddUserToDatabase(db, args.Message.From);
var checkResult = await Timeout.Check(args.Message.From.Id, db);
if (checkResult == TimeoutManager.CheckResult.Limited)
{
Locale currentLocale = Locale[(await db.Chats.FindAsync(args.Message.Chat.Id)).Locale];
await Client.SendTextMessageAsync(
args.Message.Chat.Id,
currentLocale["jetkarmabot.ratelimit"],
replyToMessageId: args.Message.MessageId);
await Timeout.SetMessaged(args.Message.From.Id, db);
return;
}
else if (checkResult != TimeoutManager.CheckResult.NonLimited)
{
return;
}
if (args.Message.ReplyToMessage != null)
await AddUserToDatabase(db, args.Message.ReplyToMessage.From);
if (!db.Chats.Any(x => x.ChatId == args.Message.Chat.Id))
db.Chats.Add(new Models.Chat
{
ChatId = args.Message.Chat.Id
});
await db.SaveChangesAsync();
}
await Commands.Execute(sender, messageEventArgs);
await Commands.Execute(cmd, args);
});
}
@ -104,7 +142,8 @@ namespace JetKarmaBot
public void Dispose()
{
Client.StopReceiving();
timeoutWaitTaskToken.Dispose();
timeoutWaitTask.Dispose();
}
#endregion

View File

@ -172,6 +172,10 @@ namespace JetKarmaBot.Models
entity.Property(e => e.Username)
.HasColumnName("username")
.HasColumnType("varchar(45)");
entity.Property(e => e.CooldownDate)
.HasColumnName("cooldowndate")
.HasColumnType("datetime");
});
}
}

View File

@ -14,6 +14,7 @@ namespace JetKarmaBot.Models
public int UserId { get; set; }
public string Username { get; set; }
public DateTime CooldownDate { get; set; }
[InverseProperty("From")]
public virtual ICollection<Award> AwardsFrom { get; set; }
[InverseProperty("To")]

View File

@ -0,0 +1,87 @@
using Perfusion;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using JetKarmaBot.Models;
using System.Threading;
namespace JetKarmaBot.Services
{
[Singleton]
public class TimeoutManager
{
public struct TimeoutStats
{
public DateTime CooldownDate;
public bool TimeoutMessaged;
}
[Inject] private KarmaContextFactory Db;
[Inject] private Config cfg;
public Dictionary<int, TimeoutStats> TimeoutCache = new Dictionary<int, TimeoutStats>();
public async Task ApplyCost(string name, int uid, KarmaContext db)
{
if (!cfg.Timeout.CommandCostsSeconds.TryGetValue(name, out var costSeconds))
if (!cfg.Timeout.CommandCostsSeconds.TryGetValue("Default", out costSeconds))
{
throw new LocalizationException("Default key not present");
}
await PopulateStats(uid, db);
DateTime debtLimit = DateTime.Now.AddSeconds(cfg.Timeout.DebtLimitSeconds);
if (TimeoutCache[uid].CooldownDate >= debtLimit)
//Programming error
throw new NotImplementedException();
TimeoutCache[uid] = new TimeoutStats()
{
CooldownDate = (TimeoutCache[uid].CooldownDate <= DateTime.Now ? DateTime.Now : TimeoutCache[uid].CooldownDate).AddSeconds(costSeconds),
TimeoutMessaged = false
};
TimeoutCache[uid] = TimeoutCache[uid];
}
public enum CheckResult
{
NonLimited, Limited, LimitedSent
}
public async Task<CheckResult> Check(int uid, KarmaContext db)
{
await PopulateStats(uid, db);
DateTime debtLimit = DateTime.Now.AddSeconds(cfg.Timeout.DebtLimitSeconds);
return TimeoutCache[uid].CooldownDate < debtLimit
? CheckResult.NonLimited
: (TimeoutCache[uid].TimeoutMessaged ? CheckResult.LimitedSent : CheckResult.Limited);
}
public async Task SetMessaged(int uid, KarmaContext db)
{
await PopulateStats(uid, db);
TimeoutCache[uid] = new TimeoutStats() { TimeoutMessaged = true, CooldownDate = TimeoutCache[uid].CooldownDate };
}
private async Task PopulateStats(int uid, KarmaContext db)
{
if (!TimeoutCache.ContainsKey(uid))
{
TimeoutCache[uid] = new TimeoutStats()
{
CooldownDate = (await db.Users.FindAsync(uid)).CooldownDate
};
}
}
public async Task Save(CancellationToken ct = default(CancellationToken))
{
using (KarmaContext db = Db.GetContext())
{
foreach (int i in TimeoutCache.Keys)
{
(await db.Users.FindAsync(new object[] { i }, ct)).CooldownDate = TimeoutCache[i].CooldownDate;
}
await db.SaveChangesAsync(ct);
}
}
public async Task SaveLoop(CancellationToken ct = default(CancellationToken))
{
while (true)
{
await Task.Delay(cfg.Timeout.SaveIntervalSeconds * 1000, ct);
await Save(ct);
}
}
}
}

View File

@ -6,6 +6,7 @@
],
"note": "This is a joke. And made with google translate.",
"strings": {
"jetkarmabot.ratelimit": "Павольны, чувак!",
"jetkarmabot.award.errawardnoreply": "Пра каго ты кажаш?",
"jetkarmabot.award.errbadusername": "Хто гэта?",
"jetkarmabot.award.errdup": "Калі ласка, спыніце батчіць ўзнагароды.",
@ -15,7 +16,6 @@
"jetkarmabot.award.awardmessage": "Ўручыў \"{0}\" {1}!",
"jetkarmabot.award.revokemessage": "Адабраў \"{0}\" у {1}!",
"jetkarmabot.award.statustext": "У {0} цяпер {1}{2}.",
"jetkarmabot.award.ratelimit": "Павольны, чувак!",
"jetkarmabot.award.help": "Уручае ачко карыстачу (або адымае)",
"jetkarmabot.award.awardtypehelp": "Тып ачкі",
"jetkarmabot.award.tohelp": "Карыстальнік, якога ўзнагародзіць",

View File

@ -5,6 +5,7 @@
"англійская"
],
"strings": {
"jetkarmabot.ratelimit": "Slow down there, turbo!",
"jetkarmabot.award.errawardnoreply": "Who are you talking about?",
"jetkarmabot.award.errbadusername": "I don't know who that is.",
"jetkarmabot.award.errdup": "Please stop batching awards.",
@ -14,7 +15,6 @@
"jetkarmabot.award.awardmessage": "Awarded a {0} to {1}!",
"jetkarmabot.award.revokemessage": "Revoked a {0} from {1}!",
"jetkarmabot.award.statustext": "{0} is at {1}{2} now.",
"jetkarmabot.award.ratelimit": "Slow down there, turbo!",
"jetkarmabot.award.help": "Awards/revokes an award to a user.",
"jetkarmabot.award.awardtypehelp": "The award to grant to/strip of the specified user",
"jetkarmabot.award.tohelp": "The user to award",

View File

@ -5,6 +5,7 @@
"руская"
],
"strings": {
"jetkarmabot.ratelimit": "Помедленней, чувак!",
"jetkarmabot.award.errawardnoreply": "О ком ты говоришь?",
"jetkarmabot.award.errbadusername": "Кто это?",
"jetkarmabot.award.errdup": "Пожалуйста, не батчайте награды.",
@ -14,7 +15,6 @@
"jetkarmabot.award.awardmessage": "Вручил \"{0}\" {1}!",
"jetkarmabot.award.revokemessage": "Отнял \"{0}\" у {1}!",
"jetkarmabot.award.statustext": "У {0} теперь {1}{2}.",
"jetkarmabot.award.ratelimit": "Помедленней, чувак!",
"jetkarmabot.award.help": "Вручает очко пользователю (или отнимает)",
"jetkarmabot.award.awardtypehelp": "Тип очка",
"jetkarmabot.award.tohelp": "Пользователь, которого наградить",