From 3cd2b672a19cd87a3262a01eab5bd36aa52a2511 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 11 Jun 2023 22:54:41 +0500 Subject: [PATCH] Move scheduled events to guild tick loop Signed-off-by: Octol1ttle --- Data/ScheduledEventData.cs | 5 +- EventResponders.cs | 221 ---------------------------- Services/Data/GuildDataService.cs | 8 +- Services/GuildUpdateService.cs | 237 +++++++++++++++++++++++++++++- Services/UtilityService.cs | 34 ++++- 5 files changed, 270 insertions(+), 235 deletions(-) diff --git a/Data/ScheduledEventData.cs b/Data/ScheduledEventData.cs index 5bb1c10..661eed0 100644 --- a/Data/ScheduledEventData.cs +++ b/Data/ScheduledEventData.cs @@ -11,6 +11,7 @@ public class ScheduledEventData { Status = status; } - public DateTimeOffset? ActualStartTime { get; set; } - public GuildScheduledEventStatus Status { get; set; } + public bool EarlyNotificationSent { get; set; } + public DateTimeOffset? ActualStartTime { get; set; } + public GuildScheduledEventStatus Status { get; set; } } diff --git a/EventResponders.cs b/EventResponders.cs index 22df360..2efe194 100644 --- a/EventResponders.cs +++ b/EventResponders.cs @@ -1,4 +1,3 @@ -using System.Text; using Boyfriend.Data; using Boyfriend.Services.Data; using DiffPlex; @@ -7,14 +6,11 @@ using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Caching; using Remora.Discord.Caching.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Discord.Gateway.Responders; -using Remora.Discord.Interactivity; -using Remora.Rest.Core; using Remora.Results; // ReSharper disable UnusedType.Global @@ -259,223 +255,6 @@ public class GuildMemberAddResponder : IResponder { } } -/// -/// Handles sending a notification, mentioning the if one is -/// set, -/// when a scheduled event is created -/// in a guild's if one is set. -/// -public class GuildScheduledEventCreateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; - - public GuildScheduledEventCreateResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, - IDiscordRestUserAPI userApi) { - _channelApi = channelApi; - _dataService = dataService; - _userApi = userApi; - } - - public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) { - var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); - guildData.ScheduledEvents.Add( - gatewayEvent.ID.Value, new ScheduledEventData(GuildScheduledEventStatus.Scheduled)); - - if (guildData.Configuration.EventNotificationChannel is 0) - return Result.FromSuccess(); - - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - if (!gatewayEvent.CreatorID.IsDefined(out var creatorId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.CreatorID))); - var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct); - if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult); - - Messages.Culture = guildData.Culture; - - string embedDescription; - var eventDescription = gatewayEvent.Description is { HasValue: true, Value: not null } - ? gatewayEvent.Description.Value - : string.Empty; - switch (gatewayEvent.EntityType) { - case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: - if (!gatewayEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); - - embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionLocalEventCreated, - Markdown.Timestamp(gatewayEvent.ScheduledStartTime), - Mention.Channel(channelId) - ))}"; - break; - case GuildScheduledEventEntityType.External: - if (!gatewayEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EntityMetadata))); - if (!gatewayEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ScheduledEndTime))); - if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); - - embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( - string.Format( - Messages.DescriptionExternalEventCreated, - Markdown.Timestamp(gatewayEvent.ScheduledStartTime), - Markdown.Timestamp(endTime), - Markdown.InlineCode(location) - ))}"; - break; - default: - return Result.FromError(new ArgumentOutOfRangeError(nameof(gatewayEvent.EntityType))); - } - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) - .WithTitle(gatewayEvent.Name) - .WithDescription(embedDescription) - .WithEventCover(gatewayEvent.ID, gatewayEvent.Image) - .WithUserFooter(currentUser) - .WithCurrentTimestamp() - .WithColour(ColorsList.Default) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - var roleMention = guildData.Configuration.EventNotificationRole is not 0 - ? Mention.Role(guildData.Configuration.EventNotificationRole.ToDiscordSnowflake()) - : string.Empty; - - var button = new ButtonComponent( - ButtonComponentStyle.Primary, - Messages.EventDetailsButton, - new PartialEmoji(Name: "📋"), - CustomIDHelpers.CreateButtonIDWithState( - "scheduled-event-details", $"{gatewayEvent.GuildID}:{gatewayEvent.ID}") - ); - - return (Result)await _channelApi.CreateMessageAsync( - guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built }, - components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); - } -} - -/// -/// Handles sending a notification, mentioning the if one is -/// set, -/// when a scheduled event has started or completed -/// in a guild's if one is set. -/// -public class GuildScheduledEventUpdateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestGuildScheduledEventAPI _eventApi; - - public GuildScheduledEventUpdateResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildScheduledEventAPI eventApi) { - _channelApi = channelApi; - _dataService = dataService; - _eventApi = eventApi; - } - - public async Task RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) { - var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); - if (guildData.Configuration.EventNotificationChannel is 0) - return Result.FromSuccess(); - if (!guildData.ScheduledEvents.TryGetValue(gatewayEvent.ID.Value, out var data)) { - guildData.ScheduledEvents.Add(gatewayEvent.ID.Value, new ScheduledEventData(gatewayEvent.Status)); - } else { - if (gatewayEvent.Status == data.Status) - return Result.FromSuccess(); - - guildData.ScheduledEvents[gatewayEvent.ID.Value].Status = gatewayEvent.Status; - } - - var embed = new EmbedBuilder(); - StringBuilder? content = null; - switch (gatewayEvent.Status) { - case GuildScheduledEventStatus.Active: - guildData.ScheduledEvents[gatewayEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; - - string embedDescription; - switch (gatewayEvent.EntityType) { - case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: - if (!gatewayEvent.ChannelID.AsOptional().IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); - - embedDescription = string.Format( - Messages.DescriptionLocalEventStarted, - Mention.Channel(channelId) - ); - break; - case GuildScheduledEventEntityType.External: - if (!gatewayEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EntityMetadata))); - if (!gatewayEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ScheduledEndTime))); - if (!metadata.Location.IsDefined(out var location)) - return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); - - embedDescription = string.Format( - Messages.DescriptionExternalEventStarted, - Markdown.InlineCode(location), - Markdown.Timestamp(endTime) - ); - break; - default: - return Result.FromError(new ArgumentOutOfRangeError(nameof(gatewayEvent.EntityType))); - } - - content = new StringBuilder(); - var receivers = guildData.Configuration.EventStartedReceivers; - var role = guildData.Configuration.EventNotificationRole.ToDiscordSnowflake(); - var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( - gatewayEvent.GuildID, gatewayEvent.ID, withMember: true, ct: ct); - if (!usersResult.IsDefined(out var users)) return Result.FromError(usersResult); - - if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0) - content.Append($"{Mention.Role(role)} "); - if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested)) - content = users.Where( - user => { - if (!user.GuildMember.IsDefined(out var member)) return true; - return !member.Roles.Contains(role); - }) - .Aggregate(content, (current, user) => current.Append($"{Mention.User(user.User)} ")); - - embed.WithTitle(string.Format(Messages.EventStarted, gatewayEvent.Name)) - .WithDescription(embedDescription) - .WithCurrentTimestamp() - .WithColour(ColorsList.Green); - break; - case GuildScheduledEventStatus.Completed: - embed.WithTitle(string.Format(Messages.EventCompleted, gatewayEvent.Name)) - .WithDescription( - string.Format( - Messages.EventDuration, - DateTimeOffset.UtcNow.Subtract( - guildData.ScheduledEvents[gatewayEvent.ID.Value].ActualStartTime - ?? gatewayEvent.ScheduledStartTime).ToString())) - .WithColour(ColorsList.Black); - - guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); - break; - case GuildScheduledEventStatus.Canceled: - case GuildScheduledEventStatus.Scheduled: - default: return Result.FromError(new ArgumentOutOfRangeError(nameof(gatewayEvent.Status))); - } - - var result = embed.WithCurrentTimestamp().Build(); - - if (!result.IsDefined(out var built)) return Result.FromError(result); - - return (Result)await _channelApi.CreateMessageAsync( - guildData.Configuration.EventNotificationChannel.ToDiscordSnowflake(), - content?.ToString() ?? default(Optional), embeds: new[] { built }, ct: ct); - } -} - /// /// Handles sending a notification when a scheduled event has been cancelled /// in a guild's if one is set. diff --git a/Services/Data/GuildDataService.cs b/Services/Data/GuildDataService.cs index f1dcf82..4c4aa88 100644 --- a/Services/Data/GuildDataService.cs +++ b/Services/Data/GuildDataService.cs @@ -91,11 +91,11 @@ public class GuildDataService : IHostedService { return (await GetData(guildId, ct)).Configuration; } - public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { + /*public async Task GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) { return (await GetData(guildId, ct)).GetMemberData(userId); - } + }*/ - public List GetGuildIds() { - return _datas.Keys.ToList(); + public IEnumerable GetGuildIds() { + return _datas.Keys; } } diff --git a/Services/GuildUpdateService.cs b/Services/GuildUpdateService.cs index ffe601f..8bb438c 100644 --- a/Services/GuildUpdateService.cs +++ b/Services/GuildUpdateService.cs @@ -1,17 +1,38 @@ +using Boyfriend.Data; using Boyfriend.Services.Data; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Interactivity; using Remora.Rest.Core; +using Remora.Results; namespace Boyfriend.Services; public class GuildUpdateService : BackgroundService { - private readonly GuildDataService _dataService; - private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestGuildScheduledEventAPI _eventApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; - public GuildUpdateService(GuildDataService dataService, IDiscordRestGuildAPI guildApi) { + public GuildUpdateService( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi, + IDiscordRestGuildScheduledEventAPI eventApi, ILogger logger, IDiscordRestUserAPI userApi, + UtilityService utility) { + _channelApi = channelApi; _dataService = dataService; _guildApi = guildApi; + _eventApi = eventApi; + _logger = logger; + _userApi = userApi; + _utility = utility; } protected override async Task ExecuteAsync(CancellationToken ct) { @@ -19,8 +40,7 @@ public class GuildUpdateService : BackgroundService { var tasks = new List(); while (await timer.WaitForNextTickAsync(ct)) { - foreach (var id in _dataService.GetGuildIds()) - tasks.Add(TickGuildAsync(id, ct)); + tasks.AddRange(_dataService.GetGuildIds().Select(id => TickGuildAsync(id, ct))); await Task.WhenAll(tasks); tasks.Clear(); @@ -31,12 +51,219 @@ public class GuildUpdateService : BackgroundService { var data = await _dataService.GetData(guildId, ct); Messages.Culture = data.Culture; + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator foreach (var memberData in data.MemberData.Values) if (DateTimeOffset.UtcNow > memberData.BannedUntil) { var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, memberData.Id.ToDiscordSnowflake(), Messages.PunishmentExpired.EncodeHeader(), ct); if (unbanResult.IsSuccess) memberData.BannedUntil = null; + else + _logger.LogWarning("Error in member data update.\n{ErrorMessage}", unbanResult.Error.Message); } + + var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); + if (!eventsResult.IsDefined(out var events)) return; + + if (data.Configuration.EventNotificationChannel is 0) return; + + foreach (var scheduledEvent in events) { + if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { + data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); + } else { + var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; + if (storedEvent.Status == scheduledEvent.Status) { + if (DateTimeOffset.UtcNow + >= scheduledEvent.ScheduledStartTime - data.Configuration.EventEarlyNotificationOffset + && !storedEvent.EarlyNotificationSent) { + var earlyResult = await SendScheduledEventStartedMessage(scheduledEvent, data, true, ct); + if (earlyResult.IsSuccess) + storedEvent.EarlyNotificationSent = true; + else + _logger.LogWarning( + "Error in scheduled event early notification sender.\n{ErrorMessage}", + earlyResult.Error.Message); + } + + continue; + } + + storedEvent.Status = scheduledEvent.Status; + } + + var result = scheduledEvent.Status switch { + GuildScheduledEventStatus.Scheduled => + await SendScheduledEventCreatedMessage(scheduledEvent, data.Configuration, ct), + GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => + await SendScheduledEventStartedMessage(scheduledEvent, data, false, ct), + _ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) + }; + + if (!result.IsSuccess) + _logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message); + } + } + + /// + /// Handles sending a notification, mentioning the if one is + /// set, + /// when a scheduled event is created + /// in a guild's if one is set. + /// + private async Task SendScheduledEventCreatedMessage( + IGuildScheduledEvent scheduledEvent, GuildConfiguration config, CancellationToken ct = default) { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + if (!scheduledEvent.CreatorID.IsDefined(out var creatorId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID))); + var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct); + if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult); + + string embedDescription; + var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } + ? scheduledEvent.Description.Value + : string.Empty; + switch (scheduledEvent.EntityType) { + case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionLocalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Mention.Channel(channelId) + ))}"; + break; + case GuildScheduledEventEntityType.External: + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( + string.Format( + Messages.DescriptionExternalEventCreated, + Markdown.Timestamp(scheduledEvent.ScheduledStartTime), + Markdown.Timestamp(endTime), + Markdown.InlineCode(location) + ))}"; + break; + default: + return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) + .WithTitle(scheduledEvent.Name) + .WithDescription(embedDescription) + .WithEventCover(scheduledEvent.ID, scheduledEvent.Image) + .WithUserFooter(currentUser) + .WithCurrentTimestamp() + .WithColour(ColorsList.White) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + var roleMention = config.EventNotificationRole is not 0 + ? Mention.Role(config.EventNotificationRole.ToDiscordSnowflake()) + : string.Empty; + + var button = new ButtonComponent( + ButtonComponentStyle.Primary, + Messages.EventDetailsButton, + new PartialEmoji(Name: "📋"), + CustomIDHelpers.CreateButtonIDWithState( + "scheduled-event-details", $"{scheduledEvent.GuildID}:{scheduledEvent.ID}") + ); + + return (Result)await _channelApi.CreateMessageAsync( + config.EventNotificationChannel.ToDiscordSnowflake(), roleMention, embeds: new[] { built }, + components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + } + + /// + /// Handles sending a notification, mentioning the s, + /// when a scheduled event is about to start, has started or completed + /// in a guild's if one is set. + /// + private async Task SendScheduledEventStartedMessage( + IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) { + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var embed = new EmbedBuilder(); + string? content = null; + if (early) + embed.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser) + .WithColour(ColorsList.Default); + else + switch (scheduledEvent.Status) { + case GuildScheduledEventStatus.Active: + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + + string embedDescription; + switch (scheduledEvent.EntityType) { + case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: + if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); + + embedDescription = string.Format( + Messages.DescriptionLocalEventStarted, + Mention.Channel(channelId) + ); + break; + case GuildScheduledEventEntityType.External: + if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata))); + if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime)) + return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime))); + if (!metadata.Location.IsDefined(out var location)) + return Result.FromError(new ArgumentNullError(nameof(metadata.Location))); + + embedDescription = string.Format( + Messages.DescriptionExternalEventStarted, + Markdown.InlineCode(location), + Markdown.Timestamp(endTime) + ); + break; + default: + return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType))); + } + + var contentResult = await _utility.GetEventNotificationMentions(data, scheduledEvent, ct); + if (!contentResult.IsDefined(out content)) + return Result.FromError(contentResult); + + embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green); + break; + case GuildScheduledEventStatus.Completed: + embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + .WithDescription( + string.Format( + Messages.EventDuration, + DateTimeOffset.UtcNow.Subtract( + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime + ?? scheduledEvent.ScheduledStartTime).ToString())) + .WithColour(ColorsList.Black); + + data.ScheduledEvents.Remove(scheduledEvent.ID.Value); + break; + case GuildScheduledEventStatus.Canceled: + case GuildScheduledEventStatus.Scheduled: + default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))); + } + + var result = embed.WithCurrentTimestamp().Build(); + + if (!result.IsDefined(out var built)) return Result.FromError(result); + + return (Result)await _channelApi.CreateMessageAsync( + data.Configuration.EventNotificationChannel.ToDiscordSnowflake(), + content ?? default(Optional), embeds: new[] { built }, ct: ct); } } diff --git a/Services/UtilityService.cs b/Services/UtilityService.cs index 7ce6da6..4c6da4d 100644 --- a/Services/UtilityService.cs +++ b/Services/UtilityService.cs @@ -1,5 +1,9 @@ +using System.Text; +using Boyfriend.Data; using Microsoft.Extensions.Hosting; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; @@ -10,12 +14,15 @@ namespace Boyfriend.Services; /// of some Discord APIs. /// public class UtilityService : IHostedService { - private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestGuildScheduledEventAPI _eventApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; - public UtilityService(IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) { + public UtilityService( + IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestGuildScheduledEventAPI eventApi) { _guildApi = guildApi; _userApi = userApi; + _eventApi = eventApi; } public Task StartAsync(CancellationToken ct) { @@ -94,4 +101,25 @@ public class UtilityService : IHostedService { return Result.FromSuccess(null); } + + public async Task> GetEventNotificationMentions( + GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default) { + var builder = new StringBuilder(); + var receivers = data.Configuration.EventStartedReceivers; + var role = data.Configuration.EventNotificationRole.ToDiscordSnowflake(); + var usersResult = await _eventApi.GetGuildScheduledEventUsersAsync( + scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); + if (!usersResult.IsDefined(out var users)) return Result.FromError(usersResult); + + if (receivers.Contains(GuildConfiguration.NotificationReceiver.Role) && role.Value is not 0) + builder.Append($"{Mention.Role(role)} "); + if (receivers.Contains(GuildConfiguration.NotificationReceiver.Interested)) + builder = users.Where( + user => { + if (!user.GuildMember.IsDefined(out var member)) return true; + return !member.Roles.Contains(role); + }) + .Aggregate(builder, (current, user) => current.Append($"{Mention.User(user.User)} ")); + return builder.ToString(); + } }