From a326adb6804645f68f06f56d5f9b5324495fc30f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:01:24 +0500 Subject: [PATCH 1/4] Fix Reminder serialization/deserialization (#166) Closes #124 Before we dive into this PR, let's explain a few rules of serialization and deserialization in System.Text.Json (often referred as STJ). The important rule of serialization is that fields are ***not*** serialized unless the serializer gets passed an instance of `JsonSerializerOptions` that explicitly tells it to include fields. However, properties are serialized by default. The important rule of ***de***serialization is that class members are only deserialized if: 1) In case of properties, they must have a setter 2) If they do not have a setter, the constructor must have an argument, required or optional, that will act as the setter for that property. Unfortunately, both of these rules were ignored in some commit that refactored reminders. This PR is here to fix that issue by: 1) Converting fields in `Reminder.cs` to properties 2) Adding an optional argument to the `MemberData` constructor --- src/Data/MemberData.cs | 6 +++++- src/Data/Reminder.cs | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index c7ddc27..b63f8ad 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,10 +5,14 @@ namespace Octobot.Data; /// public sealed class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil = null) + public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List? reminders = null) { Id = id; BannedUntil = bannedUntil; + if (reminders is not null) + { + Reminders = reminders; + } } public ulong Id { get; } diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs index 828fadb..42144f9 100644 --- a/src/Data/Reminder.cs +++ b/src/Data/Reminder.cs @@ -2,7 +2,7 @@ namespace Octobot.Data; public struct Reminder { - public DateTimeOffset At; - public string Text; - public ulong Channel; + public DateTimeOffset At { get; init; } + public string Text { get; init; } + public ulong Channel { get; init; } } From 687883bbf8294e595210f4e81003c14d23e713f8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:07:01 +0500 Subject: [PATCH 2/4] Use MemberData to determine a subscriber's role list (#165) Closes #163 Discord's API sucks a lot. You ask it for a member, but it won't give you a member. This is why this PR updates the `GetEventNotificationMentions` method used to determine what roles and users should get pinged for a scheduled event. Previously, the bot asked Discord to provide the member for each subscriber to determine whether or not they have the event notification role (to avoid pinging people personally when the role would already ping them). With this pull request, the bot uses MemberData, it's own member storage, for that purpose (if you're wondering why, refer to the first two sentences) --- src/Services/Update/ScheduledEventUpdateService.cs | 4 ++-- src/Services/UtilityService.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 20d23fa..792eef9 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -286,7 +286,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService }; var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); + scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { return Result.FromError(contentResult); @@ -412,7 +412,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { var contentResult = await _utility.GetEventNotificationMentions( - scheduledEvent, data.Settings, ct); + scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { return Result.FromError(contentResult); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index b144ca7..5bc06ea 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -160,16 +160,16 @@ public sealed class UtilityService : IHostedService /// /// The scheduled event whose subscribers will be mentioned. /// - /// The settings of the guild containing the scheduled event + /// The data of the guild containing the scheduled event. /// The cancellation token for this operation. /// A result containing the string which may or may not have succeeded. public async Task> GetEventNotificationMentions( - IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { var builder = new StringBuilder(); - var role = GuildSettings.EventNotificationRole.Get(settings); + var role = GuildSettings.EventNotificationRole.Get(data.Settings); var subscribersResult = await _eventApi.GetGuildScheduledEventUsersAsync( - scheduledEvent.GuildID, scheduledEvent.ID, withMember: true, ct: ct); + scheduledEvent.GuildID, scheduledEvent.ID, ct: ct); if (!subscribersResult.IsDefined(out var subscribers)) { return Result.FromError(subscribersResult); @@ -181,7 +181,7 @@ public sealed class UtilityService : IHostedService } builder = subscribers.Where( - subscriber => subscriber.GuildMember.IsDefined(out var member) && !member.Roles.Contains(role)) + subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); } From b30d690113f484d9cedc6ad18af20f83fac642cc Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:20:58 +0500 Subject: [PATCH 3/4] Add filtering by message author to /clear (#169) Closes #164 This PR adds an optional argument to `/clear` - `author` of type User. If the user is specified, only messages sent by that user will be cleared. Simple as that. --- locale/Messages.resx | 6 ++++++ locale/Messages.ru.resx | 6 ++++++ locale/Messages.tt-ru.resx | 6 ++++++ src/Commands/ClearCommandGroup.cs | 27 ++++++++++++++++++++++----- src/Messages.Designer.cs | 16 ++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index ab821ac..31ed7b3 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -564,4 +564,10 @@ Boost count + + There are no messages matching your filter! + + + Cleared {0} messages from {1} + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index f38032b..cb65749 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -564,4 +564,10 @@ Количество бустов + + Нет сообщений, которые подходят под твой фильтр! + + + Очищено {0} сообщений от {1} + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 52d6c7e..b5f6ad1 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -564,4 +564,10 @@ кол-во бустов + + алло а чё мне удалять-то + + + вырезано {0} забавных сообщений от {1} + diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index a6ac188..714c9de 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -45,9 +45,10 @@ public class ClearCommandGroup : CommandGroup } /// - /// A slash command that clears messages in the channel it was executed. + /// A slash command that clears messages in the channel it was executed, optionally filtering by message author. /// /// The amount of messages to clear. + /// The user whose messages will be cleared. /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages /// were cleared and vice-versa. @@ -62,7 +63,8 @@ public class ClearCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] - int amount) + int amount, + IUser? author = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -92,11 +94,11 @@ public class ClearCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ClearMessagesAsync(executor, amount, data, channelId, messages, bot, CancellationToken); + return await ClearMessagesAsync(executor, author, data, channelId, messages, bot, CancellationToken); } private async Task ClearMessagesAsync( - IUser executor, int amount, GuildData data, Snowflake channelId, IReadOnlyList messages, IUser bot, + IUser executor, IUser? author, GuildData data, Snowflake channelId, IReadOnlyList messages, IUser bot, CancellationToken ct = default) { var idList = new List(messages.Count); @@ -104,12 +106,27 @@ public class ClearCommandGroup : CommandGroup for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') { var message = messages[i]; + if (author is not null && message.Author.ID != author.ID) + { + continue; + } + idList.Add(message.ID); builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); builder.Append(message.Content.InBlockCode()); } - var title = string.Format(Messages.MessagesCleared, amount.ToString()); + if (idList.Count == 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoMessagesToClear, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct); + } + + var title = author is not null + ? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag()) + : string.Format(Messages.MessagesCleared, idList.Count.ToString()); var description = builder.ToString(); var deleteResult = await _channelApi.BulkDeleteMessagesAsync( diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ebf7fec..4a771d0 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -980,5 +980,21 @@ namespace Octobot { return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture); } } + + internal static string NoMessagesToClear + { + get + { + return ResourceManager.GetString("NoMessagesToClear", resourceCulture); + } + } + + internal static string MessagesClearedFiltered + { + get + { + return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture); + } + } } } From 580cd24810c1a96230ed1ad1e2c01ac6e026fb66 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 17 Oct 2023 17:23:14 +0500 Subject: [PATCH 4/4] Do not try to send messages in empty EventNotificationChannels (#167) This PR fixes an error that would occur if an event was created, was about to start or started and the EventNotificationChannel was empty. --- .../Update/ScheduledEventUpdateService.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index 792eef9..1672e00 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -177,6 +177,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendScheduledEventCreatedMessage( IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { + if (GuildSettings.EventNotificationChannel.Get(settings).Empty()) + { + return Result.FromSuccess(); + } + if (!scheduledEvent.Creator.IsDefined(out var creator)) { return new ArgumentNullError(nameof(scheduledEvent.Creator)); @@ -277,6 +282,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService { data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + var embedDescriptionResult = scheduledEvent.EntityType switch { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice => @@ -297,7 +307,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromError(embedDescriptionResult); } - var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, Markdown.Sanitize(scheduledEvent.Name))) + var startedEmbed = new EmbedBuilder() + .WithTitle(string.Format(Messages.EventStarted, Markdown.Sanitize(scheduledEvent.Name))) .WithDescription(embedDescription) .WithColour(ColorsList.Green) .WithCurrentTimestamp() @@ -322,7 +333,8 @@ public sealed class ScheduledEventUpdateService : BackgroundService return Result.FromSuccess(); } - var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, Markdown.Sanitize(eventData.Name))) + var completedEmbed = new EmbedBuilder() + .WithTitle(string.Format(Messages.EventCompleted, Markdown.Sanitize(eventData.Name))) .WithDescription( string.Format( Messages.EventDuration, @@ -411,6 +423,11 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task SendEarlyEventNotificationAsync( IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { + if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) + { + return Result.FromSuccess(); + } + var contentResult = await _utility.GetEventNotificationMentions( scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content))