Added reminders

This commit is contained in:
Deukhoofd 2018-08-13 18:41:59 +02:00
parent 56f1e2740e
commit b2e3b3dec8
No known key found for this signature in database
GPG Key ID: B4C087AC81641654
14 changed files with 212 additions and 26 deletions

View File

@ -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}.");
}
}
}
}
}

View File

@ -9,5 +9,6 @@
<PackageReference Include="Microsoft.NETCore.App" Version="2.1.2" /> <PackageReference Include="Microsoft.NETCore.App" Version="2.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Npgsql" Version="4.0.2" /> <PackageReference Include="Npgsql" Version="4.0.2" />
<PackageReference Include="StackExchange.Redis" Version="1.2.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -6,9 +6,12 @@ using Discord;
namespace DeukBot4 namespace DeukBot4
{ {
public static class Logger
public class Logger
{ {
private static readonly Dictionary<LogSeverity, ConsoleColor> Colors = new Dictionary<LogSeverity, ConsoleColor> public static Logger Main = new Logger();
private readonly Dictionary<LogSeverity, ConsoleColor> Colors = new Dictionary<LogSeverity, ConsoleColor>
{ {
{LogSeverity.Info, ConsoleColor.Black}, {LogSeverity.Info, ConsoleColor.Black},
{LogSeverity.Verbose, ConsoleColor.Black}, {LogSeverity.Verbose, ConsoleColor.Black},
@ -18,34 +21,33 @@ namespace DeukBot4
{LogSeverity.Critical, ConsoleColor.Red} {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.ForegroundColor = Colors[severity];
Console.WriteLine($"[{severity}] {DateTime.UtcNow:u}: {o.ToString()}"); Console.WriteLine($"[{severity}] {DateTime.UtcNow:u}: {o.ToString()}");
Console.ResetColor(); Console.ResetColor();
} }
public static async Task LogDiscord(LogMessage message) public async Task LogDiscord(LogMessage message)
{ {
Console.ForegroundColor = Colors[message.Severity]; Console.ForegroundColor = Colors[message.Severity];
Console.WriteLine($"[{message.Severity}] {DateTime.UtcNow:u}: {message.Message}"); Console.WriteLine($"[{message.Severity}] {DateTime.UtcNow:u}: {message.Message}");
Console.ResetColor(); Console.ResetColor();
} }
public static async Task Log(object o) public async Task Log(object o)
{ {
await Log(o, LogSeverity.Info); await Log(o, LogSeverity.Info);
} }
public static async Task LogWarning(object o) public async Task LogWarning(object o)
{ {
await Log(o, LogSeverity.Warning); await Log(o, LogSeverity.Warning);
} }
public static async Task LogError(object o) public async Task LogError(object o)
{ {
await Log(o, LogSeverity.Error); await Log(o, LogSeverity.Error);
} }
} }
} }

View File

@ -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(", ")}"); $"Loaded following commands for container {obj.Name}: {commands.Select(x => x.Name).Join(", ")}");
} }
@ -57,19 +57,19 @@ namespace DeukBot4.MessageHandlers.CommandHandler
switch (resultCode) switch (resultCode)
{ {
case CommandRequest.RequestCode.Invalid: case CommandRequest.RequestCode.Invalid:
await Logger.LogError("Invalid content: " + message.Content); await Logger.Main.LogError("Invalid content: " + message.Content);
return; return;
case CommandRequest.RequestCode.InvalidParameters: case CommandRequest.RequestCode.InvalidParameters:
await Logger.LogError("Invalid parameters: " + message.Content); await Logger.Main.LogError("Invalid parameters: " + message.Content);
break; break;
case CommandRequest.RequestCode.Forbidden: case CommandRequest.RequestCode.Forbidden:
await Logger.Log( await Logger.Main.Log(
$"Unauthorized user tried to run command: {message.Author.Username} -> {message.Content}"); $"Unauthorized user tried to run command: {message.Author.Username} -> {message.Content}");
break; break;
case CommandRequest.RequestCode.OK: case CommandRequest.RequestCode.OK:
if (!(message.Channel is IGuildChannel) && req.Item1.Command.ForbidInPm) 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}"); $"User is trying to use blocked command in PM: {message.Author.Username}");
return; return;
} }
@ -80,7 +80,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler
} }
catch (Exception e) catch (Exception e)
{ {
await Logger.Log("An error occured: \n" + e); await Logger.Main.Log("An error occured: \n" + e);
} }
break; break;
case CommandRequest.RequestCode.UnknownCommand: case CommandRequest.RequestCode.UnknownCommand:

View File

@ -3,6 +3,7 @@ using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using DeukBot4.APIHandlers; using DeukBot4.APIHandlers;
using DeukBot4.Database;
using DeukBot4.MessageHandlers.CommandHandler.RequestStructure; using DeukBot4.MessageHandlers.CommandHandler.RequestStructure;
using DeukBot4.MessageHandlers.Permissions; using DeukBot4.MessageHandlers.Permissions;
using DeukBot4.Utilities; using DeukBot4.Utilities;
@ -140,6 +141,5 @@ namespace DeukBot4.MessageHandlers.CommandHandler
}; };
await request.SendMessageAsync("", embed: eb.Build()); await request.SendMessageAsync("", embed: eb.Build());
} }
} }
} }

View File

@ -178,7 +178,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler
span = TimeSpan.FromMinutes(minutes.Value); span = TimeSpan.FromMinutes(minutes.Value);
break; break;
case ParameterMatcher.ParameterType.Timespan: case ParameterMatcher.ParameterType.Timespan:
var sp = TimespanParser.Parse(request.Parameters[1].AsString()); var sp = TimespanHelper.Parse(request.Parameters[1].AsString());
if (sp.HasValue) if (sp.HasValue)
{ {
span = sp.Value; span = sp.Value;

View File

@ -99,7 +99,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler
} }
catch(Exception e) catch(Exception e)
{ {
await Logger.LogError(e.Message); await Logger.Main.LogError(e.Message);
} }
} }

View File

@ -59,7 +59,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
} }
catch (Exception e) catch (Exception e)
{ {
await Logger.LogError(e.Message); await Logger.Main.LogError(e.Message);
return (null, RequestCode.Forbidden, null); return (null, RequestCode.Forbidden, null);
} }
if (permission < command.Permission) if (permission < command.Permission)

View File

@ -47,7 +47,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
case ParameterType.User: case ParameterType.User:
return $" *(?:<@!*(?<{index}>\\d+)>|(?<{index}>\\d+)(?:$| |\n))"; return $" *(?:<@!*(?<{index}>\\d+)>|(?<{index}>\\d+)(?:$| |\n))";
case ParameterType.Timespan: case ParameterType.Timespan:
return $" *(?<{index}>\\d+\\.*d*[smhd])"; return $" *(?<{index}>\\d+\\.*\\d*[smhd])";
default: default:
throw new ArgumentOutOfRangeException(nameof(type), type, null); throw new ArgumentOutOfRangeException(nameof(type), type, null);
} }
@ -66,7 +66,7 @@ namespace DeukBot4.MessageHandlers.CommandHandler.RequestStructure
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError(e.ToString()); Logger.Main.LogError(e.ToString());
return command.RequireParameterMatch ? null : new RequestParameter[0]; return command.RequireParameterMatch ? null : new RequestParameter[0];
} }
if (matches.Success) if (matches.Success)

View File

@ -76,7 +76,7 @@ namespace DeukBot4.MessageHandlers
} }
catch (Exception e) catch (Exception e)
{ {
await Logger.LogError(e); await Logger.Main.LogError(e);
} }
} }
} }

View File

@ -1,8 +1,10 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using DeukBot4.APIHandlers; using DeukBot4.APIHandlers;
using DeukBot4.Database;
using DeukBot4.Utilities; using DeukBot4.Utilities;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
@ -22,6 +24,7 @@ namespace DeukBot4.MessageHandlers
#pragma warning disable 4014 #pragma warning disable 4014
CommandHandler.CommandHandler.HandleMessage(message); CommandHandler.CommandHandler.HandleMessage(message);
HandlePrivateMessage(message); HandlePrivateMessage(message);
HandleReminder(message);
ImageBackupHandler.Backup(message); ImageBackupHandler.Backup(message);
JokeHandlers.DeltaHandler(message); JokeHandlers.DeltaHandler(message);
JokeHandlers.DadJokeHandler(message); JokeHandlers.DadJokeHandler(message);
@ -30,7 +33,7 @@ namespace DeukBot4.MessageHandlers
} }
catch (Exception e) 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) 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) if (_dmChannel == null)
{ {
_dmChannel = (ITextChannel) Program.Client.GetChannel(Program.Settings.DmChannel); _dmChannel = (ITextChannel) Program.Client.GetChannel(Program.Settings.DmChannel);
@ -63,5 +66,70 @@ namespace DeukBot4.MessageHandlers
} }
} }
private static Regex ReminderMatcher =
new Regex(
@".*(remind\s*((?<recipient>me)|<@!*(?<recipient>\d*)>)\s*to)(?<action>.*)(in\s*)(?<timeNum>\d)\s*(?<timeId>\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}");
}
} }
} }

View File

@ -38,7 +38,7 @@ namespace DeukBot4.MessageHandlers.Permissions
} }
catch(Exception e) catch(Exception e)
{ {
await Logger.LogError(e.ToString()); await Logger.Main.LogError(e.ToString());
return PermissionLevel.Everyone; return PermissionLevel.Everyone;
} }
} }

View File

@ -7,6 +7,7 @@ using DeukBot4.MessageHandlers.CommandHandler;
using Discord; using Discord;
using Discord.Commands.Builders; using Discord.Commands.Builders;
using Discord.WebSocket; using Discord.WebSocket;
using StackExchange.Redis;
namespace DeukBot4 namespace DeukBot4
{ {
@ -21,10 +22,17 @@ namespace DeukBot4
MainAsync().GetAwaiter().GetResult(); MainAsync().GetAwaiter().GetResult();
} }
private static async Task SetupScheduler()
{
ReminderHandler.Main.CheckReminders();
}
private static async Task MainAsync() private static async Task MainAsync()
{ {
Settings = Settings.FromJsonFile("settings.json"); Settings = Settings.FromJsonFile("settings.json");
DatabaseConnection.ConnectionString = Settings.DatabaseConnectionString; DatabaseConnection.ConnectionString = Settings.DatabaseConnectionString;
await SetupScheduler();
DatabaseInitializer.Initialize(); DatabaseInitializer.Initialize();
ServerSettingHandler.OnBotStartUp(); ServerSettingHandler.OnBotStartUp();
@ -35,7 +43,7 @@ namespace DeukBot4
Client = new DiscordSocketClient(); Client = new DiscordSocketClient();
Client.Log += Logger.LogDiscord; Client.Log += Logger.Main.LogDiscord;
Client.Ready += OnReady; Client.Ready += OnReady;
Client.MessageReceived += MainHandler.HandleMessage; Client.MessageReceived += MainHandler.HandleMessage;

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text;
namespace DeukBot4.Utilities namespace DeukBot4.Utilities
{ {
public static class TimespanParser public static class TimespanHelper
{ {
public static TimeSpan? Parse(string s) public static TimeSpan? Parse(string s)
{ {
@ -27,6 +28,19 @@ namespace DeukBot4.Utilities
default: default:
return null; 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();
} }
} }