mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-01-31 09:09:00 +03:00
Fix various issues with ScheduledEventUpdateService (#89)
This PR closes #85. This PR fixes many issues related to scheduled events. Most importantly, scheduled events that are no longer present in the guild, but still have data related to them, won't be left rotting. This requires deletion of `ScheduledEvents.json` files in all guilds. Maybe I'll start writing datafixers one day... Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
501c51b865
commit
4252613dd3
6 changed files with 190 additions and 112 deletions
|
@ -8,12 +8,20 @@ namespace Boyfriend.Data;
|
||||||
/// <remarks>This information is stored on disk as a JSON file.</remarks>
|
/// <remarks>This information is stored on disk as a JSON file.</remarks>
|
||||||
public sealed class ScheduledEventData
|
public sealed class ScheduledEventData
|
||||||
{
|
{
|
||||||
public ScheduledEventData(GuildScheduledEventStatus? status)
|
public ScheduledEventData(ulong id, string name, GuildScheduledEventStatus status,
|
||||||
|
DateTimeOffset scheduledStartTime)
|
||||||
{
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
Status = status;
|
Status = status;
|
||||||
|
ScheduledStartTime = scheduledStartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ulong Id { get; }
|
||||||
|
public string Name { get; set; }
|
||||||
public bool EarlyNotificationSent { get; set; }
|
public bool EarlyNotificationSent { get; set; }
|
||||||
|
public DateTimeOffset ScheduledStartTime { get; set; }
|
||||||
public DateTimeOffset? ActualStartTime { get; set; }
|
public DateTimeOffset? ActualStartTime { get; set; }
|
||||||
public GuildScheduledEventStatus? Status { get; set; }
|
public GuildScheduledEventStatus? Status { get; set; }
|
||||||
|
public bool ScheduleOnStatusUpdated { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,21 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
||||||
data.GetOrCreateMemberData(member.User.Value.ID);
|
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))
|
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||||
{
|
{
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles sending a notification when a scheduled event has been cancelled
|
|
||||||
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
|
|
||||||
/// </summary>
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class GuildScheduledEventDeleteResponder : IResponder<IGuildScheduledEventDelete>
|
|
||||||
{
|
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
|
||||||
private readonly GuildDataService _guildData;
|
|
||||||
|
|
||||||
public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService guildData)
|
|
||||||
{
|
|
||||||
_channelApi = channelApi;
|
|
||||||
_guildData = guildData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result> 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);
|
|
||||||
}
|
|
||||||
}
|
|
32
src/Responders/ScheduledEventCreatedResponder.cs
Normal file
32
src/Responders/ScheduledEventCreatedResponder.cs
Normal file
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles adding a scheduled event to a guild's ScheduledEventData.
|
||||||
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class ScheduledEventCreatedResponder : IResponder<IGuildScheduledEventCreate>
|
||||||
|
{
|
||||||
|
private readonly GuildDataService _guildData;
|
||||||
|
|
||||||
|
public ScheduledEventCreatedResponder(GuildDataService guildData)
|
||||||
|
{
|
||||||
|
_guildData = guildData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> 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();
|
||||||
|
}
|
||||||
|
}
|
30
src/Responders/ScheduledEventUpdatedResponder.cs
Normal file
30
src/Responders/ScheduledEventUpdatedResponder.cs
Normal file
|
@ -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<IGuildScheduledEventUpdate>
|
||||||
|
{
|
||||||
|
private readonly GuildDataService _guildData;
|
||||||
|
|
||||||
|
public ScheduledEventUpdatedResponder(GuildDataService guildData)
|
||||||
|
{
|
||||||
|
_guildData = guildData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,40 +61,56 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
||||||
return Result.FromError(eventsResult);
|
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.ScheduleOnStatusUpdated)
|
||||||
if (storedEvent.Status == scheduledEvent.Status)
|
|
||||||
{
|
{
|
||||||
var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct);
|
var tickResult = await TickScheduledEventAsync(guildId, data, scheduledEvent.Entity, storedEvent, ct);
|
||||||
failedResults.AddIfFailed(tickResult);
|
failedResults.AddIfFailed(tickResult);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusChangedResponseResult = scheduledEvent.Status switch
|
var statusUpdatedResponseResult = storedEvent.Status switch
|
||||||
{
|
{
|
||||||
GuildScheduledEventStatus.Scheduled =>
|
GuildScheduledEventStatus.Scheduled =>
|
||||||
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
|
await SendScheduledEventCreatedMessage(scheduledEvent.Entity, data.Settings, ct),
|
||||||
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
|
GuildScheduledEventStatus.Canceled =>
|
||||||
await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct),
|
await SendScheduledEventCancelledMessage(storedEvent, data, ct),
|
||||||
_ => new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))
|
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();
|
return failedResults.AggregateErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Result<IGuildScheduledEvent> TryGetScheduledEvent(IEnumerable<IGuildScheduledEvent> from, ulong id)
|
||||||
|
{
|
||||||
|
var filtered = from.Where(schEvent => schEvent.ID == id);
|
||||||
|
var filteredArray = filtered.ToArray();
|
||||||
|
return filteredArray.Any()
|
||||||
|
? Result<IGuildScheduledEvent>.FromSuccess(filteredArray.Single())
|
||||||
|
: new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Result> TickScheduledEventAsync(
|
private async Task<Result> TickScheduledEventAsync(
|
||||||
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData,
|
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
|
@ -240,10 +256,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
||||||
/// <param name="data">The data for the guild containing the scheduled event.</param>
|
/// <param name="data">The data for the guild containing the scheduled event.</param>
|
||||||
/// <param name="ct">The cancellation token for this operation</param>
|
/// <param name="ct">The cancellation token for this operation</param>
|
||||||
/// <returns>A reminder/notification sending result which may or may not have succeeded.</returns>
|
/// <returns>A reminder/notification sending result which may or may not have succeeded.</returns>
|
||||||
private async Task<Result> SendScheduledEventUpdatedMessage(
|
private async Task<Result> SendScheduledEventStartedMessage(
|
||||||
IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default)
|
IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default)
|
||||||
{
|
|
||||||
if (scheduledEvent.Status == GuildScheduledEventStatus.Active)
|
|
||||||
{
|
{
|
||||||
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow;
|
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
@ -283,20 +297,16 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
||||||
content, embeds: new[] { startedBuilt }, ct: ct);
|
content, embeds: new[] { startedBuilt }, ct: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scheduledEvent.Status != GuildScheduledEventStatus.Completed)
|
private async Task<Result> SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data,
|
||||||
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
return new ArgumentOutOfRangeError(nameof(scheduledEvent.Status));
|
var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, eventData.Name))
|
||||||
}
|
|
||||||
|
|
||||||
data.ScheduledEvents.Remove(scheduledEvent.ID.Value);
|
|
||||||
|
|
||||||
var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
|
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
string.Format(
|
string.Format(
|
||||||
Messages.EventDuration,
|
Messages.EventDuration,
|
||||||
DateTimeOffset.UtcNow.Subtract(
|
DateTimeOffset.UtcNow.Subtract(
|
||||||
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime
|
eventData.ActualStartTime
|
||||||
?? scheduledEvent.ScheduledStartTime).ToString()))
|
?? eventData.ScheduledStartTime).ToString()))
|
||||||
.WithColour(ColorsList.Black)
|
.WithColour(ColorsList.Black)
|
||||||
.WithCurrentTimestamp()
|
.WithCurrentTimestamp()
|
||||||
.Build();
|
.Build();
|
||||||
|
@ -306,9 +316,45 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
||||||
return Result.FromError(completedEmbed);
|
return Result.FromError(completedEmbed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Result)await _channelApi.CreateMessageAsync(
|
var createResult = (Result)await _channelApi.CreateMessageAsync(
|
||||||
GuildSettings.EventNotificationChannel.Get(data.Settings),
|
GuildSettings.EventNotificationChannel.Get(data.Settings),
|
||||||
embeds: new[] { completedBuilt }, ct: ct);
|
embeds: new[] { completedBuilt }, ct: ct);
|
||||||
|
if (createResult.IsSuccess)
|
||||||
|
{
|
||||||
|
data.ScheduledEvents.Remove(eventData.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Result> 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<string> GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent)
|
private static Result<string> GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent)
|
||||||
|
|
Loading…
Reference in a new issue