#nullable disable using LinqToDB; using LinqToDB.EntityFrameworkCore; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services.Database.Models; using System.Text.RegularExpressions; using Nadeko.Common; namespace NadekoBot.Modules.Utility.Services; public class RemindService : INService, IReadyExecutor { private readonly Regex _regex = new( @"^(?:in\s?)?\s*(?:(?\d+)(?:\s?(?:months?|mos?),?))?(?:(?:\sand\s|\s*)?(?\d+)(?:\s?(?:weeks?|w),?))?(?:(?:\sand\s|\s*)?(?\d+)(?:\s?(?:days?|d),?))?(?:(?:\sand\s|\s*)?(?\d+)(?:\s?(?:hours?|h),?))?(?:(?:\sand\s|\s*)?(?\d+)(?:\s?(?:minutes?|mins?|m),?))?\s+(?:to:?\s+)?(?(?:\r\n|[\r\n]|.)+)", RegexOptions.Compiled | RegexOptions.Multiline); private readonly DiscordSocketClient _client; private readonly DbService _db; private readonly IBotCredentials _creds; private readonly IEmbedBuilderService _eb; public RemindService( DiscordSocketClient client, DbService db, IBotCredentials creds, IEmbedBuilderService eb) { _client = client; _db = db; _creds = creds; _eb = eb; } public async Task OnReadyAsync() { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); while (await timer.WaitForNextTickAsync()) { await OnReminderLoopTickInternalAsync(); } } private async Task OnReminderLoopTickInternalAsync() { try { var now = DateTime.UtcNow; var reminders = await GetRemindersBeforeAsync(now); if (reminders.Count == 0) return; Log.Information("Executing {ReminderCount} reminders", reminders.Count); // make groups of 5, with 1.5 second inbetween each one to ensure against ratelimits foreach (var group in reminders.Chunk(5)) { var executedReminders = group.ToList(); await executedReminders.Select(ReminderTimerAction).WhenAll(); await RemoveReminders(executedReminders.Select(x => x.Id)); await Task.Delay(1500); } } catch (Exception ex) { Log.Warning(ex, "Error in reminder loop: {ErrorMessage}", ex.Message); } } private async Task RemoveReminders(IEnumerable reminders) { await using var uow = _db.GetDbContext(); await uow.Reminders .ToLinqToDBTable() .DeleteAsync(x => reminders.Contains(x.Id)); await uow.SaveChangesAsync(); } private async Task> GetRemindersBeforeAsync(DateTime now) { await using var uow = _db.GetDbContext(); return await uow.Reminders .ToLinqToDBTable() .Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId) && x.When < now) .ToListAsyncLinqToDB(); } public bool TryParseRemindMessage(string input, out RemindObject obj) { var m = _regex.Match(input); obj = default; if (m.Length == 0) return false; var values = new Dictionary(); var what = m.Groups["what"].Value; if (string.IsNullOrWhiteSpace(what)) { Log.Warning("No message provided for the reminder"); return false; } foreach (var groupName in _regex.GetGroupNames()) { if (groupName is "0" or "what") continue; if (string.IsNullOrWhiteSpace(m.Groups[groupName].Value)) { values[groupName] = 0; continue; } if (!int.TryParse(m.Groups[groupName].Value, out var value)) { Log.Warning("Reminder regex group {GroupName} has invalid value", groupName); return false; } if (value < 1) { Log.Warning("Reminder time value has to be an integer greater than 0"); return false; } values[groupName] = value; } var ts = new TimeSpan((30 * values["mo"]) + (7 * values["w"]) + values["d"], values["h"], values["m"], 0); obj = new() { Time = ts, What = what }; return true; } private async Task ReminderTimerAction(Reminder r) { try { IMessageChannel ch; if (r.IsPrivate) { var user = _client.GetUser(r.ChannelId); if (user is null) return; ch = await user.CreateDMChannelAsync(); } else ch = _client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId); if (ch is null) return; await ch.EmbedAsync(_eb.Create() .WithOkColor() .WithTitle("Reminder") .AddField("Created At", r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?") .AddField("By", (await ch.GetUserAsync(r.UserId))?.ToString() ?? r.UserId.ToString()), r.Message); } catch (Exception ex) { Log.Warning(ex, "Error executing reminder {ReminderId}: {ErrorMessage}", r.Id, ex.Message); } } public struct RemindObject { public string What { get; set; } public TimeSpan Time { get; set; } } public async Task AddReminderAsync(ulong userId, ulong targetId, ulong? guildId, bool isPrivate, DateTime time, string message) { var rem = new Reminder { UserId = userId, ChannelId = targetId, ServerId = guildId ?? 0, IsPrivate = isPrivate, When = time, Message = message, }; await using var ctx = _db.GetDbContext(); await ctx.Reminders .AddAsync(rem); await ctx.SaveChangesAsync(); } }