diff --git a/src/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs index 7cd0578..8af9c92 100644 --- a/src/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -8,12 +8,20 @@ namespace Boyfriend.Data; /// This information is stored on disk as a JSON file. public sealed class ScheduledEventData { - public ScheduledEventData(GuildScheduledEventStatus? status) + public ScheduledEventData(ulong id, string name, GuildScheduledEventStatus status, + DateTimeOffset scheduledStartTime) { + Id = id; + Name = name; Status = status; + ScheduledStartTime = scheduledStartTime; } + public ulong Id { get; } + public string Name { get; set; } public bool EarlyNotificationSent { get; set; } + public DateTimeOffset ScheduledStartTime { get; set; } public DateTimeOffset? ActualStartTime { get; set; } public GuildScheduledEventStatus? Status { get; set; } + public bool ScheduleOnStatusUpdated { get; set; } = true; } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 9dd2f97..a5b8b19 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -50,6 +50,21 @@ public class GuildLoadedResponder : IResponder data.GetOrCreateMemberData(member.User.Value.ID); } + foreach (var schEvent in guild.GuildScheduledEvents) + { + if (!data.ScheduledEvents.TryGetValue(schEvent.ID.Value, out var eventData)) + { + data.ScheduledEvents.Add(schEvent.ID.Value, new ScheduledEventData(schEvent.ID.Value, + schEvent.Name, schEvent.Status, schEvent.ScheduledStartTime)); + continue; + } + + eventData.Name = schEvent.Name; + eventData.ScheduledStartTime = schEvent.ScheduledStartTime; + eventData.ScheduleOnStatusUpdated = eventData.Status != schEvent.Status; + eventData.Status = schEvent.Status; + } + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.FromSuccess(); diff --git a/src/Responders/ScheduledEventCancelledResponder.cs b/src/Responders/ScheduledEventCancelledResponder.cs deleted file mode 100644 index c35128a..0000000 --- a/src/Responders/ScheduledEventCancelledResponder.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Boyfriend.Data; -using Boyfriend.Services; -using JetBrains.Annotations; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Boyfriend.Responders; - -/// -/// Handles sending a notification when a scheduled event has been cancelled -/// in a guild's if one is set. -/// -[UsedImplicitly] -public class GuildScheduledEventDeleteResponder : IResponder -{ - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _guildData; - - public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService guildData) - { - _channelApi = channelApi; - _guildData = guildData; - } - - public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) - { - var guildData = await _guildData.GetData(gatewayEvent.GuildID, ct); - guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); - - if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) - { - return Result.FromSuccess(); - } - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) - .WithDescription(":(") - .WithColour(ColorsList.Red) - .WithCurrentTimestamp() - .Build(); - - if (!embed.IsDefined(out var built)) - { - return Result.FromError(embed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct); - } -} diff --git a/src/Responders/ScheduledEventCreatedResponder.cs b/src/Responders/ScheduledEventCreatedResponder.cs new file mode 100644 index 0000000..36f313a --- /dev/null +++ b/src/Responders/ScheduledEventCreatedResponder.cs @@ -0,0 +1,32 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles adding a scheduled event to a guild's ScheduledEventData. +/// +[UsedImplicitly] +public class ScheduledEventCreatedResponder : IResponder +{ + private readonly GuildDataService _guildData; + + public ScheduledEventCreatedResponder(GuildDataService guildData) + { + _guildData = guildData; + } + + public async Task RespondAsync(IGuildScheduledEventCreate gatewayEvent, CancellationToken ct = default) + { + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + data.ScheduledEvents.Add(gatewayEvent.ID.Value, + new ScheduledEventData(gatewayEvent.ID.Value, + gatewayEvent.Name, gatewayEvent.Status, gatewayEvent.ScheduledStartTime)); + + return Result.FromSuccess(); + } +} diff --git a/src/Responders/ScheduledEventUpdatedResponder.cs b/src/Responders/ScheduledEventUpdatedResponder.cs new file mode 100644 index 0000000..7db7edd --- /dev/null +++ b/src/Responders/ScheduledEventUpdatedResponder.cs @@ -0,0 +1,30 @@ +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +[UsedImplicitly] +public class ScheduledEventUpdatedResponder : IResponder +{ + private readonly GuildDataService _guildData; + + public ScheduledEventUpdatedResponder(GuildDataService guildData) + { + _guildData = guildData; + } + + public async Task RespondAsync(IGuildScheduledEventUpdate gatewayEvent, CancellationToken ct = default) + { + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + var eventData = data.ScheduledEvents[gatewayEvent.ID.Value]; + eventData.Name = gatewayEvent.Name; + eventData.ScheduledStartTime = gatewayEvent.ScheduledStartTime; + eventData.ScheduleOnStatusUpdated = eventData.Status != gatewayEvent.Status; + eventData.Status = gatewayEvent.Status; + + return Result.FromSuccess(); + } +} diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 24b68da..de03fc5 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -61,40 +61,56 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(eventsResult); } - foreach (var scheduledEvent in events) + foreach (var storedEvent in data.ScheduledEvents.Values) { - if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) + var scheduledEvent = TryGetScheduledEvent(events, storedEvent.Id); + if (!scheduledEvent.IsSuccess) { - data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(null)); + storedEvent.ScheduleOnStatusUpdated = true; + storedEvent.Status = storedEvent.ActualStartTime != null + ? GuildScheduledEventStatus.Completed + : GuildScheduledEventStatus.Canceled; } - var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; - if (storedEvent.Status == scheduledEvent.Status) + if (!storedEvent.ScheduleOnStatusUpdated) { - var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct); + var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent.Entity, storedEvent, ct); failedResults.AddIfFailed(tickResult); continue; } - var statusChangedResponseResult = scheduledEvent.Status switch + var statusUpdatedResponseResult = storedEvent.Status switch { GuildScheduledEventStatus.Scheduled => - await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), - GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => - await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)) + await SendScheduledEventCreatedMessage(scheduledEvent.Entity, data.Settings, ct), + GuildScheduledEventStatus.Canceled => + await SendScheduledEventCancelledMessage(storedEvent, data, ct), + GuildScheduledEventStatus.Active => + await SendScheduledEventStartedMessage(scheduledEvent.Entity, data, ct), + GuildScheduledEventStatus.Completed => + await SendScheduledEventCompletedMessage(storedEvent, data, ct), + _ => new ArgumentOutOfRangeError(nameof(storedEvent.Status)) }; - if (statusChangedResponseResult.IsSuccess) + if (statusUpdatedResponseResult.IsSuccess) { - storedEvent.Status = scheduledEvent.Status; + storedEvent.ScheduleOnStatusUpdated = false; } - failedResults.AddIfFailed(statusChangedResponseResult); + failedResults.AddIfFailed(statusUpdatedResponseResult); } return failedResults.AggregateErrors(); } + private static Result TryGetScheduledEvent(IEnumerable from, ulong id) + { + var filtered = from.Where(schEvent => schEvent.ID == id); + var filteredArray = filtered.ToArray(); + return filteredArray.Any() + ? Result.FromSuccess(filteredArray.Single()) + : new NotFoundError(); + } + private async Task TickScheduledEventAsync( Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, CancellationToken ct) @@ -240,63 +256,57 @@ public sealed class ScheduledEventUpdateService : BackgroundService /// The data for the guild containing the scheduled event. /// The cancellation token for this operation /// A reminder/notification sending result which may or may not have succeeded. - private async Task SendScheduledEventUpdatedMessage( + private async Task SendScheduledEventStartedMessage( IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { - if (scheduledEvent.Status == GuildScheduledEventStatus.Active) + data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + + var embedDescriptionResult = scheduledEvent.EntityType switch { - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => + GetLocalEventStartedEmbedDescription(scheduledEvent), + GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), + _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) + }; - var embedDescriptionResult = scheduledEvent.EntityType switch - { - GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => - GetLocalEventStartedEmbedDescription(scheduledEvent), - GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent), - _ => new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)) - }; - - var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); - if (!contentResult.IsDefined(out var content)) - { - return Result.FromError(contentResult); - } - - if (!embedDescriptionResult.IsDefined(out var embedDescription)) - { - return Result.FromError(embedDescriptionResult); - } - - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) - .WithDescription(embedDescription) - .WithColour(ColorsList.Green) - .WithCurrentTimestamp() - .Build(); - - if (!startedEmbed.IsDefined(out var startedBuilt)) - { - return Result.FromError(startedEmbed); - } - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(data.Settings), - content, embeds: new[] { startedBuilt }, ct: ct); + var contentResult = await _utility.GetEventNotificationMentions( + scheduledEvent, data.Settings, ct); + if (!contentResult.IsDefined(out var content)) + { + return Result.FromError(contentResult); } - if (scheduledEvent.Status != GuildScheduledEventStatus.Completed) + if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)); + return Result.FromError(embedDescriptionResult); } - data.ScheduledEvents.Remove(scheduledEvent.ID.Value); + var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) + .WithDescription(embedDescription) + .WithColour(ColorsList.Green) + .WithCurrentTimestamp() + .Build(); - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name)) + if (!startedEmbed.IsDefined(out var startedBuilt)) + { + return Result.FromError(startedEmbed); + } + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), + content, embeds: new[] { startedBuilt }, ct: ct); + } + + private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, + CancellationToken ct) + { + var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, eventData.Name)) .WithDescription( string.Format( Messages.EventDuration, DateTimeOffset.UtcNow.Subtract( - data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime - ?? scheduledEvent.ScheduledStartTime).ToString())) + eventData.ActualStartTime + ?? eventData.ScheduledStartTime).ToString())) .WithColour(ColorsList.Black) .WithCurrentTimestamp() .Build(); @@ -306,9 +316,45 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(completedEmbed); } - return (Result)await _channelApi.CreateMessageAsync( + var createResult = (Result)await _channelApi.CreateMessageAsync( GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { completedBuilt }, ct: ct); + if (createResult.IsSuccess) + { + data.ScheduledEvents.Remove(eventData.Id); + } + + return createResult; + } + + private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data, + CancellationToken ct) + { + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCancelled, eventData.Name)) + .WithDescription(":(") + .WithColour(ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + + if (!embed.IsDefined(out var built)) + { + return Result.FromError(embed); + } + + var createResult = (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(data.Settings), embeds: new[] { built }, ct: ct); + if (createResult.IsSuccess) + { + data.ScheduledEvents.Remove(eventData.Id); + } + + return createResult; } private static Result GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent)