diff --git a/DeukBot4/Database/ReminderHandler.cs b/DeukBot4/Database/ReminderHandler.cs new file mode 100644 index 0000000..8503d4d --- /dev/null +++ b/DeukBot4/Database/ReminderHandler.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Discord; +using StackExchange.Redis; + +namespace DeukBot4.Database +{ + public class ReminderHandler + { + public static ReminderHandler Main = new ReminderHandler(); + public static ConnectionMultiplexer Redis = ConnectionMultiplexer.Connect("127.0.0.1"); + + + public void AddReminder(TimeSpan time, string message, ulong channel, ulong author, ulong recipient) + { + try + { + var db = Redis.GetDatabase(); + var id = Guid.NewGuid().ToString(); + var expectedTime = DateTime.UtcNow.Add(time); + db.SortedSetAdd("deukbot_reminders", (RedisValue)id, expectedTime.ToBinary()); + db.HashSet((RedisKey) id, new[] + { + new HashEntry("channel", channel), + new HashEntry("message", message), + new HashEntry("author", author), + new HashEntry("recipient", recipient), + }); + } + catch (Exception e) + { + Logger.Main.LogError(e); + } + } + + public async Task CheckReminders() + { + var checkTime = TimeSpan.FromSeconds(70); + var startTime = DateTime.UtcNow; + var desiredTopScore = (startTime + checkTime).ToBinary(); + var db = Redis.GetDatabase(); + var reminders = db.SortedSetRangeByScoreWithScores("deukbot_reminders", stop: desiredTopScore); + foreach (var sortedSetEntry in reminders) + { + var val = sortedSetEntry.Element.ToString(); + var timeLong = sortedSetEntry.Score; + var time = DateTime.FromBinary((long) timeLong); + var data = db.HashGetAll(val); + ulong channel = 0; + ulong author = 0; + ulong recipient = 0; + string message = null; + foreach (var hashEntry in data) + { + if (hashEntry.Name == "channel") channel = (ulong) hashEntry.Value; + else if (hashEntry.Name == "message") message = hashEntry.Value; + else if (hashEntry.Name == "author") author = (ulong) hashEntry.Value; + else if (hashEntry.Name == "recipient") recipient = (ulong) hashEntry.Value; + } + var diff = time - DateTime.UtcNow; + FireReminderAtTime((int) diff.TotalSeconds, channel, message, author, recipient); + db.KeyDelete(val); + } + + db.SortedSetRemoveRangeByScore("deukbot_reminders", Double.MinValue, desiredTopScore); + + await Task.Delay(checkTime); + await CheckReminders(); + } + + private async Task FireReminderAtTime(int seconds, ulong channelId, string message, ulong author, ulong recipient) + { + if (seconds > 0) + await Task.Delay(TimeSpan.FromSeconds(seconds)); + await FireReminder(channelId, message, author, recipient); + } + + private async Task FireReminder(ulong channelId, string message, ulong author, ulong recipient) + { + if (Program.Client.GetChannel(channelId) is ITextChannel channel) + { + if (author == recipient) + { + channel.SendMessageAsync($"Hey <@{recipient}>, don't forget to {message}."); + } + else + { + channel.SendMessageAsync($"Hey <@{recipient}>, <@{author}> asked me to remind you to {message}."); + } + } + } + } +} \ No newline at end of file diff --git a/DeukBot4/DeukBot4.csproj b/DeukBot4/DeukBot4.csproj index 40a67b8..e3acf92 100644 --- a/DeukBot4/DeukBot4.csproj +++ b/DeukBot4/DeukBot4.csproj @@ -9,5 +9,6 @@ + \ No newline at end of file diff --git a/DeukBot4/Logger.cs b/DeukBot4/Logger.cs index c13247f..c74fab8 100644 --- a/DeukBot4/Logger.cs +++ b/DeukBot4/Logger.cs @@ -6,9 +6,12 @@ using Discord; namespace DeukBot4 { - public static class Logger + + public class Logger { - private static readonly Dictionary Colors = new Dictionary + public static Logger Main = new Logger(); + + private readonly Dictionary Colors = new Dictionary { {LogSeverity.Info, ConsoleColor.Black}, {LogSeverity.Verbose, ConsoleColor.Black}, @@ -18,34 +21,33 @@ namespace DeukBot4 {LogSeverity.Critical, ConsoleColor.Red} }; - public static async Task Log(object o, LogSeverity severity) + public async Task Log(object o, LogSeverity severity) { Console.ForegroundColor = Colors[severity]; Console.WriteLine($"[{severity}] {DateTime.UtcNow:u}: {o.ToString()}"); Console.ResetColor(); } - public static async Task LogDiscord(LogMessage message) + public async Task LogDiscord(LogMessage message) { Console.ForegroundColor = Colors[message.Severity]; Console.WriteLine($"[{message.Severity}] {DateTime.UtcNow:u}: {message.Message}"); Console.ResetColor(); } - public static async Task Log(object o) + public async Task Log(object o) { await Log(o, LogSeverity.Info); } - public static async Task LogWarning(object o) + public async Task LogWarning(object o) { await Log(o, LogSeverity.Warning); } - public static async Task LogError(object o) + public async Task LogError(object o) { await Log(o, LogSeverity.Error); } - } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs index 8ae5f6b..a9422f1 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/CommandHandler.cs @@ -35,7 +35,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler } } - Logger.Log( + Logger.Main.Log( $"Loaded following commands for container {obj.Name}: {commands.Select(x => x.Name).Join(", ")}"); } @@ -57,19 +57,19 @@ namespace DeukBot4.MessageHandlers.CommandHandler switch (resultCode) { case CommandRequest.RequestCode.Invalid: - await Logger.LogError("Invalid content: " + message.Content); + await Logger.Main.LogError("Invalid content: " + message.Content); return; case CommandRequest.RequestCode.InvalidParameters: - await Logger.LogError("Invalid parameters: " + message.Content); + await Logger.Main.LogError("Invalid parameters: " + message.Content); break; case CommandRequest.RequestCode.Forbidden: - await Logger.Log( + await Logger.Main.Log( $"Unauthorized user tried to run command: {message.Author.Username} -> {message.Content}"); break; case CommandRequest.RequestCode.OK: if (!(message.Channel is IGuildChannel) && req.Item1.Command.ForbidInPm) { - await Logger.Log( + await Logger.Main.Log( $"User is trying to use blocked command in PM: {message.Author.Username}"); return; } @@ -80,7 +80,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler } catch (Exception e) { - await Logger.Log("An error occured: \n" + e); + await Logger.Main.Log("An error occured: \n" + e); } break; case CommandRequest.RequestCode.UnknownCommand: diff --git a/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs index e49975a..b0542e4 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Commands/GeneralCommands.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using System.Web; using DeukBot4.APIHandlers; +using DeukBot4.Database; using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; using DeukBot4.MessageHandlers.Permissions; using DeukBot4.Utilities; @@ -140,6 +141,5 @@ namespace DeukBot4.MessageHandlers.CommandHandler }; await request.SendMessageAsync("", embed: eb.Build()); } - } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs index d28effe..42f9341 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Commands/ModeratorCommands.cs @@ -178,7 +178,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler span = TimeSpan.FromMinutes(minutes.Value); break; case ParameterMatcher.ParameterType.Timespan: - var sp = TimespanParser.Parse(request.Parameters[1].AsString()); + var sp = TimespanHelper.Parse(request.Parameters[1].AsString()); if (sp.HasValue) { span = sp.Value; diff --git a/DeukBot4/MessageHandlers/CommandHandler/Commands/RolePermissionCommands.cs b/DeukBot4/MessageHandlers/CommandHandler/Commands/RolePermissionCommands.cs index 3b4ea21..dfcd782 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/Commands/RolePermissionCommands.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/Commands/RolePermissionCommands.cs @@ -99,7 +99,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler } catch(Exception e) { - await Logger.LogError(e.Message); + await Logger.Main.LogError(e.Message); } } diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs index 67f280a..d1541c5 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/CommandRequest.cs @@ -59,7 +59,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure } catch (Exception e) { - await Logger.LogError(e.Message); + await Logger.Main.LogError(e.Message); return (null, RequestCode.Forbidden, null); } if (permission < command.Permission) diff --git a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs index 65ff88b..eeed654 100644 --- a/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs +++ b/DeukBot4/MessageHandlers/CommandHandler/RequestStructure/ParameterMatcher.cs @@ -47,7 +47,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure case ParameterType.User: return $" *(?:<@!*(?<{index}>\\d+)>|(?<{index}>\\d+)(?:$| |\n))"; case ParameterType.Timespan: - return $" *(?<{index}>\\d+\\.*d*[smhd])"; + return $" *(?<{index}>\\d+\\.*\\d*[smhd])"; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } @@ -66,7 +66,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure } catch (Exception e) { - Logger.LogError(e.ToString()); + Logger.Main.LogError(e.ToString()); return command.RequireParameterMatch ? null : new RequestParameter[0]; } if (matches.Success) diff --git a/DeukBot4/MessageHandlers/ImageBackupHandler.cs b/DeukBot4/MessageHandlers/ImageBackupHandler.cs index b59576b..4914c29 100644 --- a/DeukBot4/MessageHandlers/ImageBackupHandler.cs +++ b/DeukBot4/MessageHandlers/ImageBackupHandler.cs @@ -76,7 +76,7 @@ namespace DeukBot4.MessageHandlers } catch (Exception e) { - await Logger.LogError(e); + await Logger.Main.LogError(e); } } } diff --git a/DeukBot4/MessageHandlers/MainHandler.cs b/DeukBot4/MessageHandlers/MainHandler.cs index 410a174..358c91d 100644 --- a/DeukBot4/MessageHandlers/MainHandler.cs +++ b/DeukBot4/MessageHandlers/MainHandler.cs @@ -1,8 +1,10 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using DeukBot4.APIHandlers; +using DeukBot4.Database; using DeukBot4.Utilities; using Discord; using Discord.WebSocket; @@ -22,6 +24,7 @@ namespace DeukBot4.MessageHandlers #pragma warning disable 4014 CommandHandler.CommandHandler.HandleMessage(message); HandlePrivateMessage(message); + HandleReminder(message); ImageBackupHandler.Backup(message); JokeHandlers.DeltaHandler(message); JokeHandlers.DadJokeHandler(message); @@ -30,7 +33,7 @@ namespace DeukBot4.MessageHandlers } catch (Exception e) { - await Logger.LogError(e.ToString()); + await Logger.Main.LogError(e.ToString()); } } @@ -39,7 +42,7 @@ namespace DeukBot4.MessageHandlers { if (message.Channel is IPrivateChannel) { - await Logger.Log(($"Private Message: {message.Author.Username}- {message.Content}")); + await Logger.Main.Log(($"Private Message: {message.Author.Username}- {message.Content}")); if (_dmChannel == null) { _dmChannel = (ITextChannel) Program.Client.GetChannel(Program.Settings.DmChannel); @@ -63,5 +66,70 @@ namespace DeukBot4.MessageHandlers } } + private static Regex ReminderMatcher = + new Regex( + @".*(remind\s*((?me)|<@!*(?\d*)>)\s*to)(?.*)(in\s*)(?\d)\s*(?\w*)", + RegexOptions.IgnoreCase); + private static async Task HandleReminder(SocketMessage message) + { + var match = ReminderMatcher.Match(message.Content); + if (!match.Success) + { + return; + } + var recipient = match.Groups["recipient"].Captures[0].Value; + var action = match.Groups["action"].Value.Trim(); + if (string.IsNullOrWhiteSpace(action)) + return; + var timeNumber = double.Parse(match.Groups["timeNum"].Value); + var timeIdentifier = match.Groups["timeId"].Value.Trim(); + TimeSpan timespan; + if (timeIdentifier.ToLowerInvariant().StartsWith("minu")) + { + timespan = TimeSpan.FromMinutes(timeNumber); + } + else if (timeIdentifier.ToLowerInvariant().StartsWith("hour")) + { + timespan = TimeSpan.FromHours(timeNumber); + } + else if (timeIdentifier.ToLowerInvariant().StartsWith("day")) + { + timespan = TimeSpan.FromDays(timeNumber); + } + else if (timeIdentifier.ToLowerInvariant().StartsWith("month")) + { + var dest = DateTime.UtcNow.AddMonths((int) (timeNumber)); + dest = dest.AddDays(timeNumber % 1 * 30); + timespan = dest - DateTime.UtcNow; + } + else if (timeIdentifier.ToLowerInvariant().StartsWith("year")) + { + var dest = DateTime.UtcNow.AddYears((int) (timeNumber)); + dest = dest.AddDays(timeNumber % 1 * 365); + timespan = dest - DateTime.UtcNow; + } + else + { + Logger.Main.LogError("Unknown timespan identifier: " + timeIdentifier); + return; + } + + if (timespan.TotalMinutes < 5) + { + message.Channel.SendMessageAsync("A reminder should be at least 5 minutes in the future"); + return; + } + + if (!ulong.TryParse(recipient, out var recip)) + { + recip = message.Author.Id; + } + + ReminderHandler.Main.AddReminder(timespan, action, message.Channel.Id, message.Author.Id, recip); + message.Channel.SendMessageAsync( + message.Author.Id == recip + ? $"Reminder set! I will remind you in {timespan.ToPrettyFormat()} to {action}" + : $"Reminder set! I will remind <@!{recip}> in {timespan.ToPrettyFormat()} to {action}"); + } } } \ No newline at end of file diff --git a/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs b/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs index eef3a43..477b056 100644 --- a/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs +++ b/DeukBot4/MessageHandlers/Permissions/PermissionValidator.cs @@ -38,7 +38,7 @@ namespace DeukBot4.MessageHandlers.Permissions } catch(Exception e) { - await Logger.LogError(e.ToString()); + await Logger.Main.LogError(e.ToString()); return PermissionLevel.Everyone; } } diff --git a/DeukBot4/Program.cs b/DeukBot4/Program.cs index af3be50..b66d065 100644 --- a/DeukBot4/Program.cs +++ b/DeukBot4/Program.cs @@ -7,6 +7,7 @@ using DeukBot4.MessageHandlers.CommandHandler; using Discord; using Discord.Commands.Builders; using Discord.WebSocket; +using StackExchange.Redis; namespace DeukBot4 { @@ -21,10 +22,17 @@ namespace DeukBot4 MainAsync().GetAwaiter().GetResult(); } + private static async Task SetupScheduler() + { + ReminderHandler.Main.CheckReminders(); + } + private static async Task MainAsync() { + Settings = Settings.FromJsonFile("settings.json"); DatabaseConnection.ConnectionString = Settings.DatabaseConnectionString; + await SetupScheduler(); DatabaseInitializer.Initialize(); ServerSettingHandler.OnBotStartUp(); @@ -35,7 +43,7 @@ namespace DeukBot4 Client = new DiscordSocketClient(); - Client.Log += Logger.LogDiscord; + Client.Log += Logger.Main.LogDiscord; Client.Ready += OnReady; Client.MessageReceived += MainHandler.HandleMessage; diff --git a/DeukBot4/Utilities/TimespanParser.cs b/DeukBot4/Utilities/TimespanHelper.cs similarity index 55% rename from DeukBot4/Utilities/TimespanParser.cs rename to DeukBot4/Utilities/TimespanHelper.cs index c8075d1..0e22e13 100644 --- a/DeukBot4/Utilities/TimespanParser.cs +++ b/DeukBot4/Utilities/TimespanHelper.cs @@ -1,9 +1,10 @@ using System; using System.Linq; +using System.Text; namespace DeukBot4.Utilities { - public static class TimespanParser + public static class TimespanHelper { public static TimeSpan? Parse(string s) { @@ -27,6 +28,19 @@ namespace DeukBot4.Utilities default: return null; } + } + public static string ToPrettyFormat(this TimeSpan span) { + + if (span == TimeSpan.Zero) return "0 minutes"; + + var sb = new StringBuilder(); + if (span.Days > 0) + sb.AppendFormat("{0} day{1} ", span.Days, span.Days > 1 ? "s" : String.Empty); + if (span.Hours > 0) + sb.AppendFormat("{0} hour{1} ", span.Hours, span.Hours > 1 ? "s" : String.Empty); + if (span.Minutes > 0) + sb.AppendFormat("{0} minute{1} ", span.Minutes, span.Minutes > 1 ? "s" : String.Empty); + return sb.ToString(); } }