diff --git a/src/Miha.Discord/Services/BirthdayAnnouncementService.cs b/src/Miha.Discord/Services/BirthdayAnnouncementService.cs index 3bab6ca..2204ca4 100644 --- a/src/Miha.Discord/Services/BirthdayAnnouncementService.cs +++ b/src/Miha.Discord/Services/BirthdayAnnouncementService.cs @@ -3,15 +3,24 @@ using Discord.Addons.Hosting.Util; using Discord.WebSocket; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Miha.Logic.Services.Interfaces; +using Miha.Redis.Documents; using Miha.Shared.ZonedClocks.Interfaces; namespace Miha.Discord.Services; -public class BirthdayAnnouncementService : DiscordClientService +/// +/// Announces birthdays to a channel by checking and using s in the database +/// +public partial class BirthdayAnnouncementService : DiscordClientService { private readonly DiscordSocketClient _client; + private readonly IGuildService _guildService; + private readonly IUserService _userService; + private readonly IBirthdayJobService _birthdayJobService; private readonly IEasternStandardZonedClock _easternStandardZonedClock; + private readonly DiscordOptions _discordOptions; private readonly ILogger _logger; private const string Schedule = "0,5,10,15,20,25,30,35,40,45,50,55 8-19 * * *"; // https://crontab.cronhub.io/ @@ -19,12 +28,19 @@ public class BirthdayAnnouncementService : DiscordClientService public BirthdayAnnouncementService( DiscordSocketClient client, - IEasternStandardZonedClock easternStandardZonedClock, + IGuildService guildService, + IUserService userService, IBirthdayJobService birthdayJobService, + IEasternStandardZonedClock easternStandardZonedClock, + IOptions discordOptions, ILogger logger) : base(client, logger) { _client = client; + _guildService = guildService; + _userService = userService; + _birthdayJobService = birthdayJobService; _easternStandardZonedClock = easternStandardZonedClock; + _discordOptions = discordOptions.Value; _logger = logger; _cron = CronExpression.Parse(Schedule, CronFormat.Standard); @@ -47,6 +63,82 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } await Task.Delay(nextUtc.Value - utcNow, stoppingToken); + + await AnnounceBirthdaysAsync(); } } + + private async Task AnnounceBirthdaysAsync() + { + SocketGuild guild; + + try + { + guild = Client.GetGuild(_discordOptions.Guild!.Value); + if (guild is null) + { + _logger.LogCritical("Guild is null {GuildId}", _discordOptions.Guild.Value); + return; + } + } + catch (Exception e) + { + LogError(e); + return; + } + + var birthdayAnnouncementChannel = await _guildService.GetBirthdayAnnouncementChannelAsync(guild.Id); + if (birthdayAnnouncementChannel.IsFailed) + { + // TODO + return; + } + + var jobDocuments = await _birthdayJobService.GetAllAsync(); + if (jobDocuments.IsFailed) + { + // TODO + return; + } + + var today = _easternStandardZonedClock.GetCurrentDate(); + foreach (var birthday in jobDocuments.Value.Where(s => s.BirthdayDate == today)) + { + var user = await _userService.GetAsync(birthday.UserId); + var userDoc = await _userService.GetAsync(birthday.UserId); + + if (user.IsFailed || user.Value is null) + { + continue; + } + + if (userDoc.IsFailed || userDoc.Value is null) + { + continue; + } + + if (userDoc.Value.EnableBirthday is false) + { + await _userService.UpsertAsync(userDoc.Value.Id, doc => doc.LastBirthdateAnnouncement = today); + } + + // do announcement + + var result = await _userService.UpsertAsync(userDoc.Value.Id, doc => doc.LastBirthdateAnnouncement = today); + var delete = await _birthdayJobService.DeleteAsync(birthday.Id); + } + + + // pull all birthday job documents, remove any that don't have a birth date of today (in est, should be already converted) + // for each birthday job document, we need to get the userDoc and user for it + // if the userDoc birthday is disabled, remove the birthdayjobdocument and set it as announced + // if the user has a role that isn't whitelisted, remove the job doc and set it as announced + // announce the birthday finally and set it as announced + } + + [LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = "Exception occurred in BirthdayAnnouncementService")] + public partial void LogError(Exception e); + + [LoggerMessage(EventId = 2, Level = LogLevel.Error, Message = "Failed to get the configured announcement channel")] + public partial void LogBirthdayAnnouncementChannelFailure(); } diff --git a/src/Miha.Logic/Services/GuildService.cs b/src/Miha.Logic/Services/GuildService.cs index 407e505..dc652ca 100644 --- a/src/Miha.Logic/Services/GuildService.cs +++ b/src/Miha.Logic/Services/GuildService.cs @@ -98,6 +98,44 @@ public async Task> GetAnnouncementChannelAsync(ulong? guild } } + public async Task> GetBirthdayAnnouncementChannelAsync(ulong? guildId) + { + try + { + if (guildId is null) + { + throw new ArgumentNullException(nameof(guildId)); + } + + var optionsResult = await GetAsync(guildId); + if (optionsResult.IsFailed) + { + _logger.LogWarning("Guild doesn't have any document when trying to get birthday announcement channel {GuildId}", guildId); + return optionsResult.ToResult(); + } + + var birthdayAnnouncementChannel = optionsResult.Value?.BirthdayAnnouncementChannel; + if (!birthdayAnnouncementChannel.HasValue) + { + _logger.LogDebug("Guild doesn't have a birthday announcement channel set {GuildId}", guildId); + return Result.Fail("Announcement channel not set"); + } + + if (await _client.GetChannelAsync(birthdayAnnouncementChannel.Value) is ITextChannel loggingChannel) + { + return Result.Ok(loggingChannel); + } + + _logger.LogWarning("Guild's birthday announcement channel wasn't found, or might not be a Text Channel {GuildId} {BirthdayAnnouncementChannelAnnouncementChannelId}", guildId, birthdayAnnouncementChannel.Value); + return Result.Fail("Announcement channel not found"); + } + catch (Exception e) + { + LogErrorException(e); + return Result.Fail(e.Message); + } + } + public async Task> GetAnnouncementRoleAsync(ulong? guildId) { try diff --git a/src/Miha.Logic/Services/Interfaces/IGuildService.cs b/src/Miha.Logic/Services/Interfaces/IGuildService.cs index 716aa94..87e6d7e 100644 --- a/src/Miha.Logic/Services/Interfaces/IGuildService.cs +++ b/src/Miha.Logic/Services/Interfaces/IGuildService.cs @@ -13,5 +13,6 @@ public interface IGuildService Task> GetLoggingChannelAsync(ulong? guildId); Task> GetAnnouncementChannelAsync(ulong? guildId); + Task> GetBirthdayAnnouncementChannelAsync(ulong? guildId); Task> GetAnnouncementRoleAsync(ulong? guildId); } diff --git a/src/Miha.Logic/Services/Interfaces/IUserService.cs b/src/Miha.Logic/Services/Interfaces/IUserService.cs index 12967b0..4faa4ee 100644 --- a/src/Miha.Logic/Services/Interfaces/IUserService.cs +++ b/src/Miha.Logic/Services/Interfaces/IUserService.cs @@ -1,4 +1,5 @@ -using FluentResults; +using Discord; +using FluentResults; using Miha.Redis.Documents; using NodaTime; @@ -11,6 +12,7 @@ public interface IUserService Task> UpsertAsync(ulong? userId, Action userFunc); Task DeleteAsync(ulong? userId, bool successIfNotFound = false); + Task> GetUserAsync(ulong? userId); Task>> GetAllUsersWithBirthdayForWeekAsync(LocalDate weekDate, bool includeAlreadyAnnounced); Task> UpsertVrchatUserIdAsync(ulong? userId, string vrcProfileUrl); } diff --git a/src/Miha.Logic/Services/UserService.cs b/src/Miha.Logic/Services/UserService.cs index 764ead2..cb69473 100644 --- a/src/Miha.Logic/Services/UserService.cs +++ b/src/Miha.Logic/Services/UserService.cs @@ -1,4 +1,6 @@ using System.Text.RegularExpressions; +using Discord; +using Discord.WebSocket; using FluentResults; using Microsoft.Extensions.Logging; using Miha.Logic.Services.Interfaces; @@ -11,17 +13,44 @@ namespace Miha.Logic.Services; public partial class UserService : DocumentService, IUserService { + private readonly DiscordSocketClient _client; private readonly IUserRepository _repository; private readonly ILogger _logger; public UserService( + DiscordSocketClient client, IUserRepository repository, ILogger logger) : base(repository, logger) { + _client = client; _repository = repository; _logger = logger; } + public async Task> GetUserAsync(ulong? userId) + { + try + { + if (userId is null) + { + throw new ArgumentNullException(nameof(userId)); + } + + if (await _client.GetUserAsync(userId.Value) is { } user) + { + return Result.Ok(user); + } + + _logger.LogWarning("User Id did not correspond to a known Discord user {UserId}", userId.Value); + return Result.Fail("User not found"); + } + catch (Exception e) + { + LogErrorException(e); + return Result.Fail(e.Message); + } + } + public async Task>> GetAllUsersWithBirthdayForWeekAsync(LocalDate weekDate, bool includeAlreadyAnnounced) { var weekNumberInYear = WeekYearRules.Iso.GetWeekOfWeekYear(weekDate); diff --git a/src/Miha.Redis/Documents/BirthdayJobDocument.cs b/src/Miha.Redis/Documents/BirthdayJobDocument.cs index 5217c59..8a48005 100644 --- a/src/Miha.Redis/Documents/BirthdayJobDocument.cs +++ b/src/Miha.Redis/Documents/BirthdayJobDocument.cs @@ -7,8 +7,11 @@ namespace Miha.Redis.Documents; public class BirthdayJobDocument : Document { [Indexed] - public ulong UserDocumentId { get; set; } + public ulong UserId { get; set; } + /// + /// Users birthdate converted to EST + /// [Indexed] public LocalDate BirthdayDate { get; set; } } diff --git a/src/Miha/Services/BirthdayScannerService.cs b/src/Miha/Services/BirthdayScannerService.cs index c7f4520..f3c1d96 100644 --- a/src/Miha/Services/BirthdayScannerService.cs +++ b/src/Miha/Services/BirthdayScannerService.cs @@ -75,19 +75,19 @@ private async Task ScanBirthdaysAsync() return; } - var birthdayJobs = await _birthdayJobService.GetAllAsync(); + // TODO This could be changed to query birthday job documents whose UserId matches any of the users who have birthdays this week + var birthdayJobs = await _birthdayJobService.GetAllAsync(); if (birthdayJobs.IsFailed) { _logger.LogError("Failed getting birthday jobs"); return; } - var unscheduledBirthdays = unannouncedBirthdaysThisWeek.Value.Where(user => !birthdayJobs.Value.Contains(new BirthdayJobDocument { UserDocumentId = user.Id })).ToList(); - + var unscheduledBirthdays = unannouncedBirthdaysThisWeek.Value.Where(user => !birthdayJobs.Value.Contains(new BirthdayJobDocument { UserId = user.Id })).ToList(); if (!unscheduledBirthdays.Any()) { - _logger.LogInformation("All birthdays for this week are already scheduled"); + _logger.LogDebug("All birthdays for this week are already scheduled"); } foreach (var unscheduledBirthday in unscheduledBirthdays) @@ -95,7 +95,7 @@ private async Task ScanBirthdaysAsync() var result = await _birthdayJobService.UpsertAsync(new BirthdayJobDocument { Id = unscheduledBirthday.Id, - UserDocumentId = unscheduledBirthday.Id, + UserId = unscheduledBirthday.Id, BirthdayDate = unscheduledBirthday.GetBirthdateInEst(today.Year)!.Value });