From df982d53aa1c0c286c1a154b301170bc1a605659 Mon Sep 17 00:00:00 2001 From: Basique Evangelist Date: Sat, 7 Dec 2019 13:38:07 +0000 Subject: [PATCH] Basic leaky bucket timeout --- JetKarmaBot/CommandRouter.cs | 32 +++----- JetKarmaBot/Commands/AwardCommand.cs | 22 +++--- JetKarmaBot/Commands/ChangeLocaleCommand.cs | 15 ++-- JetKarmaBot/Commands/CurrenciesCommand.cs | 2 + JetKarmaBot/Commands/HelpCommand.cs | 2 + JetKarmaBot/Commands/LeaderboardCommand.cs | 2 + JetKarmaBot/Commands/StatusCommand.cs | 2 + JetKarmaBot/Config.cs | 15 +++- JetKarmaBot/JetKarmaBot.cs | 57 +++++++++++--- JetKarmaBot/Models/KarmaContext.cs | 4 + JetKarmaBot/Models/User.cs | 1 + JetKarmaBot/Services/TimeoutManager.cs | 87 +++++++++++++++++++++ JetKarmaBot/lang/be-BY.json | 4 +- JetKarmaBot/lang/en-US.json | 4 +- JetKarmaBot/lang/ru-RU.json | 4 +- 15 files changed, 199 insertions(+), 54 deletions(-) create mode 100644 JetKarmaBot/Services/TimeoutManager.cs diff --git a/JetKarmaBot/CommandRouter.cs b/JetKarmaBot/CommandRouter.cs index c9f2629..164d7bb 100644 --- a/JetKarmaBot/CommandRouter.cs +++ b/JetKarmaBot/CommandRouter.cs @@ -23,33 +23,23 @@ namespace JetKarmaBot Me = await Client.GetMeAsync(); } - public Task Execute(object sender, MessageEventArgs args) + public Task 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 + try + { + if (commands.ContainsKey(cmd.Command)) { - if (commands.ContainsKey(cmd.Command)) - { - log.Debug($"Handling message via {commands[cmd.Command].GetType().Name}"); - return commands[cmd.Command].Execute(cmd, args); - } - } - catch (Exception e) - { - log.Error($"Error while handling command {cmd.Command}!"); - log.Error(e); + log.Debug($"Handling message via {commands[cmd.Command].GetType().Name}"); + return commands[cmd.Command].Execute(cmd, args); } } + catch (Exception e) + { + log.Error($"Error while handling command {cmd.Command}!"); + log.Error(e); + } return Task.FromResult(false); } diff --git a/JetKarmaBot/Commands/AwardCommand.cs b/JetKarmaBot/Commands/AwardCommand.cs index 15fc7e8..56e541d 100644 --- a/JetKarmaBot/Commands/AwardCommand.cs +++ b/JetKarmaBot/Commands/AwardCommand.cs @@ -15,8 +15,8 @@ namespace JetKarmaBot.Commands class AwardCommand : IChatCommand { public IReadOnlyCollection Names => new[] { "award", "revoke" }; - [Inject] - private Logger log; + [Inject] private Logger log; + [Inject] private TimeoutManager Timeout; public async Task 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; } } diff --git a/JetKarmaBot/Commands/ChangeLocaleCommand.cs b/JetKarmaBot/Commands/ChangeLocaleCommand.cs index 99464e6..f2d3cda 100644 --- a/JetKarmaBot/Commands/ChangeLocaleCommand.cs +++ b/JetKarmaBot/Commands/ChangeLocaleCommand.cs @@ -12,8 +12,8 @@ namespace JetKarmaBot.Commands class LocaleCommand : IChatCommand { public IReadOnlyCollection Names => new[] { "changelocale", "locale" }; - [Inject] - private Logger log; + [Inject] private Logger log; + [Inject] private TimeoutManager Timeout; public async Task 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; @@ -56,9 +59,10 @@ namespace JetKarmaBot.Commands catch (LocalizationException e) { await Client.SendTextMessageAsync( - 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); + 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; } } diff --git a/JetKarmaBot/Commands/CurrenciesCommand.cs b/JetKarmaBot/Commands/CurrenciesCommand.cs index 27c3572..a4a452a 100644 --- a/JetKarmaBot/Commands/CurrenciesCommand.cs +++ b/JetKarmaBot/Commands/CurrenciesCommand.cs @@ -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 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; } } diff --git a/JetKarmaBot/Commands/HelpCommand.cs b/JetKarmaBot/Commands/HelpCommand.cs index 01b2ac8..4c67a02 100644 --- a/JetKarmaBot/Commands/HelpCommand.cs +++ b/JetKarmaBot/Commands/HelpCommand.cs @@ -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 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( diff --git a/JetKarmaBot/Commands/LeaderboardCommand.cs b/JetKarmaBot/Commands/LeaderboardCommand.cs index ac81e45..2b55495 100644 --- a/JetKarmaBot/Commands/LeaderboardCommand.cs +++ b/JetKarmaBot/Commands/LeaderboardCommand.cs @@ -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."; diff --git a/JetKarmaBot/Commands/StatusCommand.cs b/JetKarmaBot/Commands/StatusCommand.cs index 33e87d8..f9db04b 100644 --- a/JetKarmaBot/Commands/StatusCommand.cs +++ b/JetKarmaBot/Commands/StatusCommand.cs @@ -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"; diff --git a/JetKarmaBot/Config.cs b/JetKarmaBot/Config.cs index aceec07..18d3562 100644 --- a/JetKarmaBot/Config.cs +++ b/JetKarmaBot/Config.cs @@ -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 CommandCostsSeconds { get; private set; } = new Dictionary() + { + {"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 diff --git a/JetKarmaBot/JetKarmaBot.cs b/JetKarmaBot/JetKarmaBot.cs index 77b9ecf..f027b9d 100644 --- a/JetKarmaBot/JetKarmaBot.cs +++ b/JetKarmaBot/JetKarmaBot.cs @@ -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 diff --git a/JetKarmaBot/Models/KarmaContext.cs b/JetKarmaBot/Models/KarmaContext.cs index 179c5e4..8703f2e 100644 --- a/JetKarmaBot/Models/KarmaContext.cs +++ b/JetKarmaBot/Models/KarmaContext.cs @@ -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"); }); } } diff --git a/JetKarmaBot/Models/User.cs b/JetKarmaBot/Models/User.cs index 36e65ee..4ca90aa 100644 --- a/JetKarmaBot/Models/User.cs +++ b/JetKarmaBot/Models/User.cs @@ -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 AwardsFrom { get; set; } [InverseProperty("To")] diff --git a/JetKarmaBot/Services/TimeoutManager.cs b/JetKarmaBot/Services/TimeoutManager.cs new file mode 100644 index 0000000..a959ba0 --- /dev/null +++ b/JetKarmaBot/Services/TimeoutManager.cs @@ -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 TimeoutCache = new Dictionary(); + 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 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); + } + } + } +} \ No newline at end of file diff --git a/JetKarmaBot/lang/be-BY.json b/JetKarmaBot/lang/be-BY.json index fd17081..7777c34 100644 --- a/JetKarmaBot/lang/be-BY.json +++ b/JetKarmaBot/lang/be-BY.json @@ -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": "Карыстальнік, якога ўзнагародзіць", @@ -52,4 +52,4 @@ "jetkarmabot.awardtypes.accusative.determination": "DETERMINATION", "jetkarmabot.awardtypes.accusative.raisin": "разыначкі" } -} +} \ No newline at end of file diff --git a/JetKarmaBot/lang/en-US.json b/JetKarmaBot/lang/en-US.json index 7f83b8e..fddf439 100644 --- a/JetKarmaBot/lang/en-US.json +++ b/JetKarmaBot/lang/en-US.json @@ -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", @@ -51,4 +51,4 @@ "jetkarmabot.awardtypes.accusative.determination": "DETERMINATION", "jetkarmabot.awardtypes.accusative.raisin": "raisin" } -} +} \ No newline at end of file diff --git a/JetKarmaBot/lang/ru-RU.json b/JetKarmaBot/lang/ru-RU.json index 560ec56..aacf2ba 100644 --- a/JetKarmaBot/lang/ru-RU.json +++ b/JetKarmaBot/lang/ru-RU.json @@ -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": "Пользователь, которого наградить", @@ -51,4 +51,4 @@ "jetkarmabot.awardtypes.accusative.determination": "РЕШИТЕЛЬНОСТЬ", "jetkarmabot.awardtypes.accusative.raisin": "изюм" } -} +} \ No newline at end of file