Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Birthday announcements #33

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 94 additions & 2 deletions src/Miha.Discord/Services/BirthdayAnnouncementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,44 @@
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
/// <summary>
/// Announces birthdays to a channel by checking and using <see cref="BirthdayJobDocument"/>s in the database
/// </summary>
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<BirthdayAnnouncementService> _logger;
private const string Schedule = "0,5,10,15,20,25,30,35,40,45,50,55 8-19 * * *"; // https://crontab.cronhub.io/

private readonly CronExpression _cron;

public BirthdayAnnouncementService(
DiscordSocketClient client,
IEasternStandardZonedClock easternStandardZonedClock,
IGuildService guildService,
IUserService userService,
IBirthdayJobService birthdayJobService,
IEasternStandardZonedClock easternStandardZonedClock,
IOptions<DiscordOptions> discordOptions,
ILogger<BirthdayAnnouncementService> 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);
Expand All @@ -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();
}
38 changes: 38 additions & 0 deletions src/Miha.Logic/Services/GuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,44 @@ public async Task<Result<ITextChannel>> GetAnnouncementChannelAsync(ulong? guild
}
}

public async Task<Result<ITextChannel>> 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<ITextChannel>();
}

var birthdayAnnouncementChannel = optionsResult.Value?.BirthdayAnnouncementChannel;
if (!birthdayAnnouncementChannel.HasValue)
{
_logger.LogDebug("Guild doesn't have a birthday announcement channel set {GuildId}", guildId);
return Result.Fail<ITextChannel>("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<ITextChannel>("Announcement channel not found");
}
catch (Exception e)
{
LogErrorException(e);
return Result.Fail<ITextChannel>(e.Message);
}
}

public async Task<Result<IRole>> GetAnnouncementRoleAsync(ulong? guildId)
{
try
Expand Down
1 change: 1 addition & 0 deletions src/Miha.Logic/Services/Interfaces/IGuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ public interface IGuildService

Task<Result<ITextChannel>> GetLoggingChannelAsync(ulong? guildId);
Task<Result<ITextChannel>> GetAnnouncementChannelAsync(ulong? guildId);
Task<Result<ITextChannel>> GetBirthdayAnnouncementChannelAsync(ulong? guildId);
Task<Result<IRole>> GetAnnouncementRoleAsync(ulong? guildId);
}
4 changes: 3 additions & 1 deletion src/Miha.Logic/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentResults;
using Discord;
using FluentResults;
using Miha.Redis.Documents;
using NodaTime;

Expand All @@ -11,6 +12,7 @@ public interface IUserService
Task<Result<UserDocument?>> UpsertAsync(ulong? userId, Action<UserDocument> userFunc);
Task<Result> DeleteAsync(ulong? userId, bool successIfNotFound = false);

Task<Result<IUser>> GetUserAsync(ulong? userId);
Task<Result<IEnumerable<UserDocument>>> GetAllUsersWithBirthdayForWeekAsync(LocalDate weekDate, bool includeAlreadyAnnounced);
Task<Result<UserDocument?>> UpsertVrchatUserIdAsync(ulong? userId, string vrcProfileUrl);
}
29 changes: 29 additions & 0 deletions src/Miha.Logic/Services/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using Discord;
using Discord.WebSocket;
using FluentResults;
using Microsoft.Extensions.Logging;
using Miha.Logic.Services.Interfaces;
Expand All @@ -11,17 +13,44 @@ namespace Miha.Logic.Services;

public partial class UserService : DocumentService<UserDocument>, IUserService
{
private readonly DiscordSocketClient _client;
private readonly IUserRepository _repository;
private readonly ILogger<UserService> _logger;

public UserService(
DiscordSocketClient client,
IUserRepository repository,
ILogger<UserService> logger) : base(repository, logger)
{
_client = client;
_repository = repository;
_logger = logger;
}

public async Task<Result<IUser>> 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<IUser>("User not found");
}
catch (Exception e)
{
LogErrorException(e);
return Result.Fail<IUser>(e.Message);
}
}

public async Task<Result<IEnumerable<UserDocument>>> GetAllUsersWithBirthdayForWeekAsync(LocalDate weekDate, bool includeAlreadyAnnounced)
{
var weekNumberInYear = WeekYearRules.Iso.GetWeekOfWeekYear(weekDate);
Expand Down
5 changes: 4 additions & 1 deletion src/Miha.Redis/Documents/BirthdayJobDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ namespace Miha.Redis.Documents;
public class BirthdayJobDocument : Document
{
[Indexed]
public ulong UserDocumentId { get; set; }
public ulong UserId { get; set; }

/// <summary>
/// Users birthdate converted to EST
/// </summary>
[Indexed]
public LocalDate BirthdayDate { get; set; }
}
10 changes: 5 additions & 5 deletions src/Miha/Services/BirthdayScannerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,27 @@ 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)
{
var result = await _birthdayJobService.UpsertAsync(new BirthdayJobDocument
{
Id = unscheduledBirthday.Id,
UserDocumentId = unscheduledBirthday.Id,
UserId = unscheduledBirthday.Id,
BirthdayDate = unscheduledBirthday.GetBirthdateInEst(today.Year)!.Value
});

Expand Down
Loading