From 5105b43eff654839edc1d126fa8f33972bfee75b Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:42:37 +0300 Subject: [PATCH 01/20] Add WelcomeMessagesChannel setting (#269) Closes #232 Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 5 +++++ src/Data/Options/AllOptionsEnum.cs | 1 + src/Messages.Designer.cs | 7 +++++++ src/Responders/GuildMemberJoinedResponder.cs | 4 ++-- 8 files changed, 25 insertions(+), 2 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index ca48fba..c8ef510 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -657,4 +657,7 @@ Example of a valid input: `1h30m` + + Welcome messages channel + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 7423347..eb8e57b 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -657,4 +657,7 @@ Пример правильного ввода: `1ч30м` + + Канал для приветствий + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index dc3bb6f..df50d8d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -657,4 +657,7 @@ правильно пишут так: `1h30m` + + канал куда говорить здравствуйте + diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index acfb8ed..86f031f 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -46,6 +46,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.RenameHoistedUsers, GuildSettings.PublicFeedbackChannel, GuildSettings.PrivateFeedbackChannel, + GuildSettings.WelcomeMessagesChannel, GuildSettings.EventNotificationChannel, GuildSettings.DefaultRole, GuildSettings.MuteRole, diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index cdaede6..5a99505 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -56,6 +56,11 @@ public static class GuildSettings /// public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); + /// + /// Controls what channel should welcome messages be sent to. + /// + public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel"); + public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole"); diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index a96a9ac..e9637d6 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -21,6 +21,7 @@ public enum AllOptionsEnum [UsedImplicitly] RenameHoistedUsers, [UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel, + [UsedImplicitly] WelcomeMessagesChannel, [UsedImplicitly] EventNotificationChannel, [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index ca460cf..5ad741e 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1184,5 +1184,12 @@ namespace Octobot { return ResourceManager.GetString("TimeSpanExample", resourceCulture); } } + + internal static string SettingsWelcomeMessagesChannel + { + get { + return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); + } + } } } diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index eee93b6..012bfad 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -51,7 +51,7 @@ public class GuildMemberJoinedResponder : IResponder return Result.FromError(returnRolesResult.Error); } - if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.FromSuccess(); @@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder .Build(); return await _channelApi.CreateMessageWithEmbedResultAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed, + GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } From 398abad27796eb1ee57ec382ad5fd46331fe930f Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:23:42 +0300 Subject: [PATCH 02/20] Remove unused IDiscordRestChannelAPI in ToolsCommandGroup (#273) Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- src/Commands/ToolsCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index ea91e1e..9a7c9b4 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -35,7 +35,7 @@ public class ToolsCommandGroup : CommandGroup public ToolsCommandGroup( ICommandContext context, IFeedbackService feedback, GuildDataService guildData, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi) + IDiscordRestUserAPI userApi) { _context = context; _guildData = guildData; From 1894b063aeb2fc611202a32ad068a59679099a1b Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:26:04 +0300 Subject: [PATCH 03/20] Fix auto-unban log spam (#271) Closes #255 Signed-off-by: mctaylors --- src/Services/Update/MemberUpdateService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 7674bbe..dfe8219 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -151,6 +151,13 @@ public sealed partial class MemberUpdateService : BackgroundService return Result.FromSuccess(); } + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct); + if (!existingBanResult.IsDefined()) + { + data.BannedUntil = null; + return Result.FromSuccess(); + } + var unbanResult = await _guildApi.RemoveGuildBanAsync( guildId, id, Messages.PunishmentExpired.EncodeHeader(), ct); if (unbanResult.IsSuccess) From 771750c92282d61bcd1421ee406c92bd97331706 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:27:35 +0300 Subject: [PATCH 04/20] Rename locale Sound to Loaded for clarity (#270) If you go through the locales, sooner or later you will notice `Sound*`, which is used in `GuildLoadedResponder.cs`. A new contributor (most likely) will not understand what it is used for at once, because we use `$"Loaded{i}".Localized()` instead of `Messages.Sound*` directly. Also, if you change the locale's value, for example the same "Loaded!", `Sound` will not fit anymore, because "Loaded!" is not a sound, but a phrase. Other suggestions are welcome. Signed-off-by: mctaylors --- locale/Messages.resx | 6 +++--- locale/Messages.ru.resx | 6 +++--- locale/Messages.tt-ru.resx | 6 +++--- src/Messages.Designer.cs | 12 ++++++------ src/Responders/GuildLoadedResponder.cs | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index c8ef510..c2b9abb 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -117,13 +117,13 @@ {0}, welcome to {1} - + Veemo! - + Woomy! - + Ngyes! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index eb8e57b..814106a 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + Виимо! - + Вууми! - + Нгьес! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index df50d8d..71357ad 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + вииимо! - + вуууми! - + нгьес! diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 5ad741e..4694254 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -66,21 +66,21 @@ namespace Octobot { } } - internal static string Sound1 { + internal static string Loaded1 { get { - return ResourceManager.GetString("Sound1", resourceCulture); + return ResourceManager.GetString("Loaded1", resourceCulture); } } - internal static string Sound2 { + internal static string Loaded2 { get { - return ResourceManager.GetString("Sound2", resourceCulture); + return ResourceManager.GetString("Loaded2", resourceCulture); } } - internal static string Sound3 { + internal static string Loaded3 { get { - return ResourceManager.GetString("Sound3", resourceCulture); + return ResourceManager.GetString("Loaded3", resourceCulture); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a1e7d16..fd289fc 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -88,7 +88,7 @@ public class GuildLoadedResponder : IResponder var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Sound{i}".Localized()) + .WithTitle($"Loaded{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() .WithColour(ColorsList.Blue) From 2342116e87af94406cfc2d745a55739f174b7676 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:51:32 +0300 Subject: [PATCH 05/20] Add GitInfo NuGet package (#268) In this PR, I added a NuGet package called GitInfo. It can replace Octobot.RepositoryUrl and display the bot version as the current branch and commit. --------- Signed-off-by: mctaylors Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- Octobot.csproj | 2 + locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 3 ++ src/BuildInfo.cs | 52 +++++++++++++++++++ src/Commands/AboutCommandGroup.cs | 5 +- .../Events/ErrorLoggingPostExecutionEvent.cs | 2 +- src/Messages.Designer.cs | 9 +++- src/Octobot.cs | 3 -- src/Responders/GuildLoadedResponder.cs | 2 +- 10 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/BuildInfo.cs diff --git a/Octobot.csproj b/Octobot.csproj index ab76400..bdfb46a 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -17,10 +17,12 @@ en A general-purpose Discord bot for moderation written in C# docs/octobot.ico + false + diff --git a/locale/Messages.resx b/locale/Messages.resx index c2b9abb..c2be4cd 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -657,6 +657,9 @@ Example of a valid input: `1h30m` + + Version: {0} + Welcome messages channel diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 814106a..d38509c 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -657,6 +657,9 @@ Пример правильного ввода: `1ч30м` + + Версия: {0} + Канал для приветствий diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 71357ad..dfb1ee6 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -657,6 +657,9 @@ правильно пишут так: `1h30m` + + {0} + канал куда говорить здравствуйте diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs new file mode 100644 index 0000000..50f86a2 --- /dev/null +++ b/src/BuildInfo.cs @@ -0,0 +1,52 @@ +namespace Octobot; + +public static class BuildInfo +{ + public static string RepositoryUrl + { + get + { + return ThisAssembly.Git.RepositoryUrl; + } + } + + public static string IssuesUrl + { + get + { + return $"{RepositoryUrl}/issues"; + } + } + + private static string Commit + { + get + { + return ThisAssembly.Git.Commit; + } + } + + private static string Branch + { + get + { + return ThisAssembly.Git.Branch; + } + } + + private static bool IsDirty + { + get + { + return ThisAssembly.Git.IsDirty; + } + } + + public static string Version + { + get + { + return IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; + } + } +} diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index e978ec9..05b1855 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -101,20 +101,21 @@ public class AboutCommandGroup : CommandGroup .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") + .WithFooter(string.Format(Messages.Version, BuildInfo.Version)) .Build(); var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenRepository, new PartialEmoji(Name: "🌐"), - URL: Octobot.RepositoryUrl + URL: BuildInfo.RepositoryUrl ); var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 87cfc84..5d7830b 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -72,7 +72,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 4694254..707c814 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1185,8 +1185,13 @@ namespace Octobot { } } - internal static string SettingsWelcomeMessagesChannel - { + internal static string Version { + get { + return ResourceManager.GetString("Version", resourceCulture); + } + } + + internal static string SettingsWelcomeMessagesChannel { get { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } diff --git a/src/Octobot.cs b/src/Octobot.cs index 1ebf7c3..e0d9b07 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -22,9 +22,6 @@ namespace Octobot; public sealed class Octobot { - public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; - public const string IssuesUrl = $"{RepositoryUrl}/issues"; - public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index fd289fc..c493910 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -117,7 +117,7 @@ public class GuildLoadedResponder : IResponder ButtonComponentStyle.Link, Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: Octobot.IssuesUrl + URL: BuildInfo.IssuesUrl ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, From cd75780582d82cb53c252054ddf287f9aa721f4d Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:18:40 +0300 Subject: [PATCH 06/20] Add "Member left" message (#234) Closes #231 --------- Signed-off-by: mctaylors Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- locale/Messages.resx | 6 ++ locale/Messages.ru.resx | 6 ++ locale/Messages.tt-ru.resx | 6 ++ src/Commands/BanCommandGroup.cs | 8 ++- src/Commands/KickCommandGroup.cs | 6 +- src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 16 ++++- src/Data/Options/AllOptionsEnum.cs | 1 + src/Messages.Designer.cs | 24 ++++--- src/Responders/GuildMemberLeftResponder.cs | 73 ++++++++++++++++++++++ 10 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 src/Responders/GuildMemberLeftResponder.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index c2be4cd..dfaff85 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -585,6 +585,12 @@ Report an issue + + See you soon, {0}! + + + Leave message + Time specified incorrectly! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d38509c..6169a13 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -585,6 +585,12 @@ Сообщить о проблеме + + До скорой встречи, {0}! + + + Сообщение о выходе + Неправильно указано время! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index dfb1ee6..3118e74 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -585,6 +585,12 @@ зарепортить баг + + ну, мы потеряли {0} + + + до свидания (типо настройка) + ты там правильно напиши таймспан diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index c350729..30cff14 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -181,17 +181,19 @@ public class BanCommandGroup : CommandGroup await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.BannedUntil + = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; + var banResult = await _guildApi.CreateGuildBanAsync( guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct); if (!banResult.IsSuccess) { + memberData.BannedUntil = null; return Result.FromError(banResult.Error); } - var memberData = data.GetOrCreateMemberData(target.ID); - memberData.BannedUntil - = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue; memberData.Roles.Clear(); var embed = new EmbedBuilder().WithSmallTitle( diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 0faa1d3..59c4045 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -143,17 +143,19 @@ public class KickCommandGroup : CommandGroup await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.Kicked = true; + var kickResult = await _guildApi.RemoveGuildMemberAsync( guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(), ct); if (!kickResult.IsSuccess) { + memberData.Kicked = false; return Result.FromError(kickResult.Error); } - var memberData = data.GetOrCreateMemberData(target.ID); memberData.Roles.Clear(); - memberData.Kicked = true; var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 86f031f..97ebc32 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -39,6 +39,7 @@ public class SettingsCommandGroup : CommandGroup [ GuildSettings.Language, GuildSettings.WelcomeMessage, + GuildSettings.LeaveMessage, GuildSettings.ReceiveStartupMessages, GuildSettings.RemoveRolesOnMute, GuildSettings.ReturnRolesOnRejoin, diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 5a99505..518465b 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -13,17 +13,29 @@ public static class GuildSettings public static readonly LanguageOption Language = new("Language", "en"); /// - /// Controls what message should be sent in when a new member joins the server. + /// Controls what message should be sent in when a new member joins the guild. /// /// /// /// No message will be sent if set to "off", "disable" or "disabled". - /// will be sent if set to "default" or "reset" + /// will be sent if set to "default" or "reset". /// /// /// public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); + /// + /// Controls what message should be sent in when a member leaves the guild. + /// + /// + /// + /// No message will be sent if set to "off", "disable" or "disabled". + /// will be sent if set to "default" or "reset". + /// + /// + /// + public static readonly Option LeaveMessage = new("LeaveMessage", "default"); + /// /// Controls whether or not the message should be sent /// in on startup. diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index e9637d6..6932822 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -14,6 +14,7 @@ public enum AllOptionsEnum { [UsedImplicitly] Language, [UsedImplicitly] WelcomeMessage, + [UsedImplicitly] LeaveMessage, [UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] RemoveRolesOnMute, [UsedImplicitly] ReturnRolesOnRejoin, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 707c814..0fa4820 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1037,18 +1037,26 @@ namespace Octobot { } } - internal static string InvalidTimeSpan - { - get - { + internal static string DefaultLeaveMessage { + get { + return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture); + } + } + + internal static string SettingsLeaveMessage { + get { + return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); + } + } + + internal static string InvalidTimeSpan { + get { return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); } } - internal static string UserInfoKicked - { - get - { + internal static string UserInfoKicked { + get { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs new file mode 100644 index 0000000..5c6db36 --- /dev/null +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -0,0 +1,73 @@ +using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; +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 Octobot.Responders; + +/// +/// Handles sending a guild's if one is set. +/// +/// +[UsedImplicitly] +public class GuildMemberLeftResponder : IResponder +{ + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + + public GuildMemberLeftResponder( + IDiscordRestChannelAPI channelApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi) + { + _channelApi = channelApi; + _guildData = guildData; + _guildApi = guildApi; + } + + public async Task RespondAsync(IGuildMemberRemove gatewayEvent, CancellationToken ct = default) + { + var user = gatewayEvent.User; + var data = await _guildData.GetData(gatewayEvent.GuildID, ct); + var cfg = data.Settings; + + var memberData = data.GetOrCreateMemberData(user.ID); + if (memberData.BannedUntil is not null || memberData.Kicked) + { + return Result.FromSuccess(); + } + + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() + || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") + { + return Result.FromSuccess(); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + var leaveMessage = GuildSettings.LeaveMessage.Get(cfg) is "default" or "reset" + ? Messages.DefaultLeaveMessage + : GuildSettings.LeaveMessage.Get(cfg); + + var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(leaveMessage, user.GetTag(), guild.Name), user) + .WithGuildFooter(guild) + .WithTimestamp(DateTimeOffset.UtcNow) + .WithColour(ColorsList.Black) + .Build(); + + return await _channelApi.CreateMessageWithEmbedResultAsync( + GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, + allowedMentions: Octobot.NoMentions, ct: ct); + } +} + From ecb92a318cf1fbe8a655f46e6ac53eaaa5bfa474 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:22:54 +0300 Subject: [PATCH 07/20] Fix /ping not showing correct locale (#276) yeah... --------- Signed-off-by: mctaylors --- locale/Messages.resx | 6 +++--- locale/Messages.ru.resx | 6 +++--- locale/Messages.tt-ru.resx | 6 +++--- src/Commands/PingCommandGroup.cs | 2 +- src/Messages.Designer.cs | 12 ++++++------ src/Responders/GuildLoadedResponder.cs | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index dfaff85..de99b4e 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -117,13 +117,13 @@ {0}, welcome to {1} - + Veemo! - + Woomy! - + Ngyes! diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 6169a13..991ffcc 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + Виимо! - + Вууми! - + Нгьес! diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 3118e74..a5ba94d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -117,13 +117,13 @@ {0}, добро пожаловать на сервер {1} - + вииимо! - + вуууми! - + нгьес! diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index 31fa6dc..f74e5e2 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -91,7 +91,7 @@ public class PingCommandGroup : CommandGroup } var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized()) + .WithTitle($"Generic{Random.Shared.Next(1, 4)}".Localized()) .WithDescription($"{latency:F0}{Messages.Milliseconds}") .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) .WithCurrentTimestamp() diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 0fa4820..7b44c44 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -66,21 +66,21 @@ namespace Octobot { } } - internal static string Loaded1 { + internal static string Generic1 { get { - return ResourceManager.GetString("Loaded1", resourceCulture); + return ResourceManager.GetString("Generic1", resourceCulture); } } - internal static string Loaded2 { + internal static string Generic2 { get { - return ResourceManager.GetString("Loaded2", resourceCulture); + return ResourceManager.GetString("Generic2", resourceCulture); } } - internal static string Loaded3 { + internal static string Generic3 { get { - return ResourceManager.GetString("Loaded3", resourceCulture); + return ResourceManager.GetString("Generic3", resourceCulture); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index c493910..a0636f6 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -88,7 +88,7 @@ public class GuildLoadedResponder : IResponder var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) - .WithTitle($"Loaded{i}".Localized()) + .WithTitle($"Generic{i}".Localized()) .WithDescription(Messages.Ready) .WithCurrentTimestamp() .WithColour(ColorsList.Blue) From ca49231c86f833abc13a9d15de44e68fad2aad63 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:59:25 +0300 Subject: [PATCH 08/20] Disable issue report button if dirty version detected (#275) In this PR, I'm disabling the Report Issue button if a "dirty" version is detected. This is done just in case so that "smart" developers don't accidentally report a bug that they themselves created. --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/BuildInfo.cs | 2 +- src/Commands/AboutCommandGroup.cs | 7 +++++-- src/Commands/Events/ErrorLoggingPostExecutionEvent.cs | 7 +++++-- src/Messages.Designer.cs | 6 ++++++ src/Responders/GuildLoadedResponder.cs | 7 +++++-- 8 files changed, 31 insertions(+), 7 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index de99b4e..f7500b6 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -669,4 +669,7 @@ Welcome messages channel + + Can't report an issue in the development version + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 991ffcc..e28b405 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -669,4 +669,7 @@ Канал для приветствий + + Нельзя сообщить о проблеме в версии под разработкой + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index a5ba94d..58e1178 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -669,4 +669,7 @@ канал куда говорить здравствуйте + + вот иди сам и почини что сломал + diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index 50f86a2..c704328 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -34,7 +34,7 @@ public static class BuildInfo } } - private static bool IsDirty + public static bool IsDirty { get { diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 05b1855..ebfe0f2 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -113,9 +113,12 @@ public class AboutCommandGroup : CommandGroup var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 5d7830b..6f39690 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -70,9 +70,12 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _feedback.SendContextualEmbedResultAsync(embed, diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 7b44c44..3d452a6 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1204,5 +1204,11 @@ namespace Octobot { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } } + + internal static string ButtonDirty { + get { + return ResourceManager.GetString("ButtonDirty", resourceCulture); + } + } } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index a0636f6..e60dd4f 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -115,9 +115,12 @@ public class GuildLoadedResponder : IResponder var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonReportIssue, + BuildInfo.IsDirty + ? Messages.ButtonDirty + : Messages.ButtonReportIssue, new PartialEmoji(Name: "⚠️"), - URL: BuildInfo.IssuesUrl + URL: BuildInfo.IssuesUrl, + IsDisabled: BuildInfo.IsDirty ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, From 0a930dcab16c990c5a25cd9df95430ff5c347a7f Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 21:08:59 +0500 Subject: [PATCH 09/20] Allow using expression-bodied properties (#280) This change significantly reduces the code space used by properties while maintaining clarity on types. Since only properties are allowed to use expression bodies, it is clear to developers that what they are looking at is a property or not Signed-off-by: Octol1ttle --- .editorconfig | 2 +- src/BuildInfo.cs | 48 ++++++------------------------------------------ 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/.editorconfig b/.editorconfig index ff9c068..adbec5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,7 +42,7 @@ csharp_space_between_square_brackets = false csharp_style_expression_bodied_accessors = false:warning csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_methods = false:warning -csharp_style_expression_bodied_properties = false:warning +csharp_style_expression_bodied_properties = true:warning csharp_style_namespace_declarations = file_scoped:warning csharp_style_prefer_utf8_string_literals = true:warning csharp_style_var_elsewhere = true:warning diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index c704328..369feff 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -2,51 +2,15 @@ public static class BuildInfo { - public static string RepositoryUrl - { - get - { - return ThisAssembly.Git.RepositoryUrl; - } - } + public static string RepositoryUrl => ThisAssembly.Git.RepositoryUrl; - public static string IssuesUrl - { - get - { - return $"{RepositoryUrl}/issues"; - } - } + public static string IssuesUrl => $"{RepositoryUrl}/issues"; - private static string Commit - { - get - { - return ThisAssembly.Git.Commit; - } - } + private static string Commit => ThisAssembly.Git.Commit; - private static string Branch - { - get - { - return ThisAssembly.Git.Branch; - } - } + private static string Branch => ThisAssembly.Git.Branch; - public static bool IsDirty - { - get - { - return ThisAssembly.Git.IsDirty; - } - } + public static bool IsDirty => ThisAssembly.Git.IsDirty; - public static string Version - { - get - { - return IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; - } - } + public static string Version => IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}"; } From 5cc04e9cc32e23c2f2ba4d936c0f965545913d4e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 22:34:22 +0500 Subject: [PATCH 10/20] Change position of "Jump to ..." action for better consistency (#279) This will better align with how a normal moderator would respond to the log: Before: "see log" -> "jump to message without knowing what changed" -> "read message diff" After: "see log" -> "read message diff" -> "jump to message for context" In addition, the change improves consistency with how reminders are shown. --------- Signed-off-by: Octol1ttle --- src/Responders/MessageDeletedResponder.cs | 9 +++++---- src/Responders/MessageEditedResponder.cs | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index bfedb22..52be8c9 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -83,10 +83,11 @@ public class MessageDeletedResponder : IResponder Messages.Culture = GuildSettings.Language.Get(cfg); - var builder = new StringBuilder().AppendLine( - string.Format(Messages.DescriptionActionJumpToChannel, - Mention.Channel(gatewayEvent.ChannelID))) - .AppendLine(message.Content.InBlockCode()); + var builder = new StringBuilder() + .AppendLine(message.Content.InBlockCode()) + .AppendLine( + string.Format(Messages.DescriptionActionJumpToChannel, Mention.Channel(gatewayEvent.ChannelID)) + ); var embed = new EmbedBuilder() .WithSmallTitle( diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index c7426d2..dd3f51d 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -101,10 +101,11 @@ public class MessageEditedResponder : IResponder Messages.Culture = GuildSettings.Language.Get(cfg); - var builder = new StringBuilder().AppendLine( - string.Format(Messages.DescriptionActionJumpToMessage, - $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")) - .AppendLine(diff.AsMarkdown()); + var builder = new StringBuilder() + .AppendLine(diff.AsMarkdown()) + .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage, + $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") + ); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) From 309d9000673723d5dc2c799a5cd41bfac8e35b2c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 20 Mar 2024 23:08:16 +0500 Subject: [PATCH 11/20] Log result failures with stack traces (#282) This feature will improve the debugging experience for developers by providing the information about *where exactly* a result has failed --------- Signed-off-by: Octol1ttle --- src/Attributes/StaticCallersOnlyAttribute.cs | 8 +++ src/Commands/AboutCommandGroup.cs | 2 +- src/Commands/BanCommandGroup.cs | 16 ++--- src/Commands/ClearCommandGroup.cs | 8 +-- .../Events/ErrorLoggingPostExecutionEvent.cs | 7 ++- src/Commands/KickCommandGroup.cs | 10 +-- src/Commands/MuteCommandGroup.cs | 18 +++--- src/Commands/PingCommandGroup.cs | 4 +- src/Commands/RemindCommandGroup.cs | 14 ++--- src/Commands/SettingsCommandGroup.cs | 10 +-- src/Commands/ToolsCommandGroup.cs | 16 ++--- src/Extensions/ChannelApiExtensions.cs | 2 +- src/Extensions/FeedbackServiceExtensions.cs | 2 +- src/Extensions/ResultExtensions.cs | 61 +++++++++++++++++++ src/Octobot.cs | 5 ++ src/Responders/GuildLoadedResponder.cs | 6 +- src/Responders/GuildMemberJoinedResponder.cs | 4 +- src/Responders/GuildMemberLeftResponder.cs | 3 +- src/Responders/MessageDeletedResponder.cs | 6 +- src/Services/Update/MemberUpdateService.cs | 4 +- .../Update/ScheduledEventUpdateService.cs | 10 +-- 21 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 src/Attributes/StaticCallersOnlyAttribute.cs create mode 100644 src/Extensions/ResultExtensions.cs diff --git a/src/Attributes/StaticCallersOnlyAttribute.cs b/src/Attributes/StaticCallersOnlyAttribute.cs new file mode 100644 index 0000000..e8787bf --- /dev/null +++ b/src/Attributes/StaticCallersOnlyAttribute.cs @@ -0,0 +1,8 @@ +namespace Octobot.Attributes; + +/// +/// Any property marked with should only be accessed by static methods. +/// Such properties may be used to provide dependencies where it is not possible to acquire them through normal means. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class StaticCallersOnlyAttribute : Attribute; diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index ebfe0f2..b37b2f0 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -73,7 +73,7 @@ public class AboutCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 30cff14..ef5e9a4 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -88,19 +88,19 @@ public class BanCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guild.ID, CancellationToken); @@ -144,7 +144,7 @@ public class BanCommandGroup : CommandGroup = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -191,7 +191,7 @@ public class BanCommandGroup : CommandGroup if (!banResult.IsSuccess) { memberData.BannedUntil = null; - return Result.FromError(banResult.Error); + return ResultExtensions.FromError(banResult); } memberData.Roles.Clear(); @@ -242,14 +242,14 @@ public class BanCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } // Needed to get the tag and avatar var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -276,7 +276,7 @@ public class BanCommandGroup : CommandGroup ct); if (!unbanResult.IsSuccess) { - return Result.FromError(unbanResult.Error); + return ResultExtensions.FromError(unbanResult); } data.GetOrCreateMemberData(target.ID).BannedUntil = null; diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 395810f..70afede 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -75,20 +75,20 @@ public class ClearCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var messagesResult = await _channelApi.GetChannelMessagesAsync( channelId, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) { - return Result.FromError(messagesResult); + return ResultExtensions.FromError(messagesResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -133,7 +133,7 @@ public class ClearCommandGroup : CommandGroup channelId, idList, executor.GetTag().EncodeHeader(), ct); if (!deleteResult.IsSuccess) { - return Result.FromError(deleteResult.Error); + return ResultExtensions.FromError(deleteResult); } _utility.LogAction( diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 6f39690..7e97e5c 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -59,7 +59,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent var botResult = await _userApi.GetCurrentUserAsync(ct); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot) @@ -78,10 +78,11 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent IsDisabled: BuildInfo.IsDirty ); - return await _feedback.SendContextualEmbedResultAsync(embed, + return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { new ActionRowComponent(new[] { issuesButton }) - }), ct); + }), ct) + ); } } diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 59c4045..5149ad4 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -80,19 +80,19 @@ public class KickCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -118,7 +118,7 @@ public class KickCommandGroup : CommandGroup = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -152,7 +152,7 @@ public class KickCommandGroup : CommandGroup if (!kickResult.IsSuccess) { memberData.Kicked = false; - return Result.FromError(kickResult.Error); + return ResultExtensions.FromError(kickResult); } memberData.Roles.Clear(); diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c2542e8..5ac6a04 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -85,13 +85,13 @@ public class MuteCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -130,7 +130,7 @@ public class MuteCommandGroup : CommandGroup guildId, executor.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -146,7 +146,7 @@ public class MuteCommandGroup : CommandGroup var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); if (!muteMethodResult.IsSuccess) { - return muteMethodResult; + return ResultExtensions.FromError(muteMethodResult); } var title = string.Format(Messages.UserMuted, target.GetTag()); @@ -257,14 +257,14 @@ public class MuteCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } // Needed to get the tag and avatar var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -291,7 +291,7 @@ public class MuteCommandGroup : CommandGroup guildId, executor.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } if (interactionResult.Entity is not null) @@ -324,14 +324,14 @@ public class MuteCommandGroup : CommandGroup await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken); if (!removeMuteRoleAsync.IsSuccess) { - return Result.FromError(removeMuteRoleAsync.Error); + return ResultExtensions.FromError(removeMuteRoleAsync); } var removeTimeoutResult = await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken); if (!removeTimeoutResult.IsSuccess) { - return Result.FromError(removeTimeoutResult.Error); + return ResultExtensions.FromError(removeTimeoutResult); } var title = string.Format(Messages.UserUnmuted, target.GetTag()); diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index f74e5e2..d64c6dd 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -64,7 +64,7 @@ public class PingCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -84,7 +84,7 @@ public class PingCommandGroup : CommandGroup channelId, limit: 1, ct: ct); if (!lastMessageResult.IsDefined(out var lastMessage)) { - return Result.FromError(lastMessageResult); + return ResultExtensions.FromError(lastMessageResult); } latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index f9c006e..aa1ef7e 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -63,13 +63,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -134,13 +134,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -226,13 +226,13 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -343,7 +343,7 @@ public class RemindCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var data = await _guildData.GetData(guildId, CancellationToken); diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 97ebc32..30150ee 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -98,7 +98,7 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -181,13 +181,13 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -254,7 +254,7 @@ public class SettingsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var cfg = await _guildData.GetSettings(guildId, CancellationToken); @@ -274,7 +274,7 @@ public class SettingsCommandGroup : CommandGroup var resetResult = option.Reset(cfg); if (!resetResult.IsSuccess) { - return Result.FromError(resetResult.Error); + return ResultExtensions.FromError(resetResult); } var embed = new EmbedBuilder().WithSmallTitle( diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 9a7c9b4..cc3c2cf 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -81,13 +81,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -274,13 +274,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -353,7 +353,7 @@ public class ToolsCommandGroup : CommandGroup var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -439,13 +439,13 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { - return Result.FromError(executorResult); + return ResultExtensions.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); @@ -524,7 +524,7 @@ public class ToolsCommandGroup : CommandGroup var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } var data = await _guildData.GetData(guildId, CancellationToken); diff --git a/src/Extensions/ChannelApiExtensions.cs b/src/Extensions/ChannelApiExtensions.cs index 12ccf35..99eff67 100644 --- a/src/Extensions/ChannelApiExtensions.cs +++ b/src/Extensions/ChannelApiExtensions.cs @@ -20,7 +20,7 @@ public static class ChannelApiExtensions { if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) { - return Result.FromError(embedResult.Value); + return ResultExtensions.FromError(embedResult.Value); } return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed }, diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs index 40e0d53..e6ef376 100644 --- a/src/Extensions/FeedbackServiceExtensions.cs +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -13,7 +13,7 @@ public static class FeedbackServiceExtensions { if (!embedResult.IsDefined(out var embed)) { - return Result.FromError(embedResult); + return ResultExtensions.FromError(embedResult); } return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct); diff --git a/src/Extensions/ResultExtensions.cs b/src/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..f456dac --- /dev/null +++ b/src/Extensions/ResultExtensions.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Remora.Results; + +namespace Octobot.Extensions; + +public static class ResultExtensions +{ + public static Result FromError(Result result) + { + LogResultStackTrace(result); + + return result; + } + + public static Result FromError(Result result) + { + var casted = (Result)result; + LogResultStackTrace(casted); + + return casted; + } + + [Conditional("DEBUG")] + private static void LogResultStackTrace(Result result) + { + if (Octobot.StaticLogger is null || result.IsSuccess) + { + return; + } + + Octobot.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", + result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace()); + + var inner = result.Inner; + while (inner is { IsSuccess: false }) + { + Octobot.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", + inner.Error.GetType().FullName, inner.Error.Message); + + inner = inner.Inner; + } + } + + private static string ConstructStackTrace() + { + var stackArray = new StackTrace(3, true).ToString().Split(Environment.NewLine).ToList(); + for (var i = stackArray.Count - 1; i >= 0; i--) + { + var frame = stackArray[i]; + var trimmed = frame.TrimStart(); + if (trimmed.StartsWith("at System.Threading", StringComparison.Ordinal) + || trimmed.StartsWith("at System.Runtime.CompilerServices", StringComparison.Ordinal)) + { + stackArray.RemoveAt(i); + } + } + + return string.Join(Environment.NewLine, stackArray); + } +} diff --git a/src/Octobot.cs b/src/Octobot.cs index e0d9b07..a4871f4 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Attributes; using Octobot.Commands.Events; using Octobot.Services; using Octobot.Services.Update; @@ -25,10 +26,14 @@ public sealed class Octobot public static readonly AllowedMentions NoMentions = new( Array.Empty(), Array.Empty(), Array.Empty()); + [StaticCallersOnly] + public static ILogger? StaticLogger { get; private set; } + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; + StaticLogger = services.GetRequiredService>(); var slashService = services.GetRequiredService(); // Providing a guild ID to this call will result in command duplicates! diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index e60dd4f..80e8735 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -57,7 +57,7 @@ public class GuildLoadedResponder : IResponder var botResult = await _userApi.GetCurrentUserAsync(ct); if (!botResult.IsDefined(out var bot)) { - return Result.FromError(botResult); + return ResultExtensions.FromError(botResult); } if (data.DataLoadFailed) @@ -68,7 +68,7 @@ public class GuildLoadedResponder : IResponder var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct); if (!ownerResult.IsDefined(out var owner)) { - return Result.FromError(ownerResult); + return ResultExtensions.FromError(ownerResult); } _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members", @@ -103,7 +103,7 @@ public class GuildLoadedResponder : IResponder var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); if (!channelResult.IsDefined(out var channel)) { - return Result.FromError(channelResult); + return ResultExtensions.FromError(channelResult); } var errorEmbed = new EmbedBuilder() diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 012bfad..05ceb2f 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -48,7 +48,7 @@ public class GuildMemberJoinedResponder : IResponder var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); if (!returnRolesResult.IsSuccess) { - return Result.FromError(returnRolesResult.Error); + return ResultExtensions.FromError(returnRolesResult); } if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() @@ -65,7 +65,7 @@ public class GuildMemberJoinedResponder : IResponder var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var embed = new EmbedBuilder() diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs index 5c6db36..b029c96 100644 --- a/src/Responders/GuildMemberLeftResponder.cs +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -55,7 +55,7 @@ public class GuildMemberLeftResponder : IResponder var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); if (!guildResult.IsDefined(out var guild)) { - return Result.FromError(guildResult); + return ResultExtensions.FromError(guildResult); } var embed = new EmbedBuilder() @@ -70,4 +70,3 @@ public class GuildMemberLeftResponder : IResponder allowedMentions: Octobot.NoMentions, ct: ct); } } - diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 52be8c9..7d788a7 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -51,7 +51,7 @@ public class MessageDeletedResponder : IResponder var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); if (!messageResult.IsDefined(out var message)) { - return Result.FromError(messageResult); + return ResultExtensions.FromError(messageResult); } if (string.IsNullOrWhiteSpace(message.Content)) @@ -63,7 +63,7 @@ public class MessageDeletedResponder : IResponder guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); if (!auditLogResult.IsDefined(out var auditLogPage)) { - return Result.FromError(auditLogResult); + return ResultExtensions.FromError(auditLogResult); } var auditLog = auditLogPage.AuditLogEntries.Single(); @@ -78,7 +78,7 @@ public class MessageDeletedResponder : IResponder if (!deleterResult.IsDefined(out var deleter)) { - return Result.FromError(deleterResult); + return ResultExtensions.FromError(deleterResult); } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index dfe8219..a21cdd4 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -97,7 +97,7 @@ public sealed partial class MemberUpdateService : BackgroundService = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); if (!interactionResult.IsSuccess) { - return Result.FromError(interactionResult); + return ResultExtensions.FromError(interactionResult); } var canInteract = interactionResult.Entity is null; @@ -247,7 +247,7 @@ public sealed partial class MemberUpdateService : BackgroundService reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct); if (!messageResult.IsSuccess) { - return messageResult; + return ResultExtensions.FromError(messageResult); } data.Reminders.Remove(reminder); diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index ac5c109..bce9996 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -53,7 +53,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); if (!eventsResult.IsDefined(out var events)) { - return Result.FromError(eventsResult); + return ResultExtensions.FromError(eventsResult); } SyncScheduledEvents(data, events); @@ -204,7 +204,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return Result.FromError(embedDescriptionResult); + return ResultExtensions.FromError(embedDescriptionResult); } var embed = new EmbedBuilder() @@ -298,12 +298,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { - return Result.FromError(contentResult); + return ResultExtensions.FromError(contentResult); } if (!embedDescriptionResult.IsDefined(out var embedDescription)) { - return Result.FromError(embedDescriptionResult); + return ResultExtensions.FromError(embedDescriptionResult); } var startedEmbed = new EmbedBuilder() @@ -416,7 +416,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService scheduledEvent, data, ct); if (!contentResult.IsDefined(out var content)) { - return Result.FromError(contentResult); + return ResultExtensions.FromError(contentResult); } var earlyResult = new EmbedBuilder() From a80debf1b17b3d03f27e548be85bf77261ad0f5e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 21 Mar 2024 20:55:34 +0500 Subject: [PATCH 12/20] Use Result.Success property instead of Result.FromSuccess() (#283) Signed-off-by: Octol1ttle --- .../Events/ErrorLoggingPostExecutionEvent.cs | 4 ++-- .../Events/LoggingPreparationErrorEvent.cs | 2 +- src/Commands/MuteCommandGroup.cs | 16 ++++++++++------ src/Data/Options/BoolOption.cs | 2 +- src/Data/Options/Option.cs | 14 +++++++------- src/Data/Options/SnowflakeOption.cs | 2 +- src/Data/Options/TimeSpanOption.cs | 2 +- src/Extensions/CollectionExtensions.cs | 2 +- src/Extensions/GuildScheduledEventExtensions.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 6 +++--- src/Responders/GuildMemberJoinedResponder.cs | 4 ++-- src/Responders/GuildMemberLeftResponder.cs | 4 ++-- src/Responders/GuildUnloadedResponder.cs | 2 +- src/Responders/MessageDeletedResponder.cs | 6 +++--- src/Responders/MessageEditedResponder.cs | 14 +++++++------- src/Responders/MessageReceivedResponder.cs | 2 +- src/Services/Update/MemberUpdateService.cs | 17 +++++++++-------- .../Update/ScheduledEventUpdateService.cs | 12 ++++++------ 18 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 7e97e5c..5fa2ea8 100644 --- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -20,8 +20,8 @@ namespace Octobot.Commands.Events; [UsedImplicitly] public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { - private readonly ILogger _logger; private readonly IFeedbackService _feedback; + private readonly ILogger _logger; private readonly IDiscordRestUserAPI _userApi; public ErrorLoggingPostExecutionEvent(ILogger logger, IFeedbackService feedback, @@ -53,7 +53,7 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent if (result.IsSuccess) { - return Result.FromSuccess(); + return Result.Success; } var botResult = await _userApi.GetCurrentUserAsync(ct); diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs index be48e74..87b4090 100644 --- a/src/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -33,6 +33,6 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent { _logger.LogResult(preparationResult, "Error in slash command preparation."); - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 5ac6a04..8e79830 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -118,7 +118,8 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); } - return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, + CancellationToken); } private async Task MuteUserAsync( @@ -143,14 +144,16 @@ public class MuteCommandGroup : CommandGroup var until = DateTimeOffset.UtcNow.Add(duration); // >:) - var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); + var muteMethodResult = + await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct); if (!muteMethodResult.IsSuccess) { return ResultExtensions.FromError(muteMethodResult); } var title = string.Format(Messages.UserMuted, target.GetTag()); - var description = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) + var description = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) .AppendBulletPoint(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); @@ -348,11 +351,12 @@ public class MuteCommandGroup : CommandGroup } private async Task RemoveMuteRoleAsync( - IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, CancellationToken ct = default) + IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, + CancellationToken ct = default) { if (memberData.MutedUntil is null) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( @@ -372,7 +376,7 @@ public class MuteCommandGroup : CommandGroup { if (communicationDisabledUntil is null) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index 130687e..6876164 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -20,7 +20,7 @@ public sealed class BoolOption : Option } settings[Name] = value; - return Result.FromSuccess(); + return Result.Success; } private static bool TryParseBool(string from, out bool value) diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index 0ba8ce1..5d703a8 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -35,7 +35,13 @@ public class Option : IOption public virtual Result Set(JsonNode settings, string from) { settings[Name] = from; - return Result.FromSuccess(); + return Result.Success; + } + + public Result Reset(JsonNode settings) + { + settings[Name] = null; + return Result.Success; } /// @@ -48,10 +54,4 @@ public class Option : IOption var property = settings[Name]; return property != null ? property.GetValue() : DefaultValue; } - - public Result Reset(JsonNode settings) - { - settings[Name] = null; - return Result.FromSuccess(); - } } diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index 66ada96..7118da8 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -32,7 +32,7 @@ public sealed partial class SnowflakeOption : Option } settings[Name] = parsed; - return Result.FromSuccess(); + return Result.Success; } [GeneratedRegex("[^0-9]")] diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index c81a02d..d237b6e 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -22,6 +22,6 @@ public sealed class TimeSpanOption : Option } settings[Name] = span.ToString(); - return Result.FromSuccess(); + return Result.Success; } } diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs index 9c873f2..2369532 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -32,7 +32,7 @@ public static class CollectionExtensions { return list.Count switch { - 0 => Result.FromSuccess(), + 0 => Result.Success, 1 => list[0], _ => new AggregateError(list.Cast().ToArray()) }; diff --git a/src/Extensions/GuildScheduledEventExtensions.cs b/src/Extensions/GuildScheduledEventExtensions.cs index e3217e3..f1b6985 100644 --- a/src/Extensions/GuildScheduledEventExtensions.cs +++ b/src/Extensions/GuildScheduledEventExtensions.cs @@ -22,7 +22,7 @@ public static class GuildScheduledEventExtensions } return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) - ? Result.FromSuccess() + ? Result.Success : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); } } diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index 80e8735..cd40134 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -42,7 +42,7 @@ public class GuildLoadedResponder : IResponder { if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild { - return Result.FromSuccess(); + return Result.Success; } var guild = gatewayEvent.Guild.AsT0; @@ -76,12 +76,12 @@ public class GuildLoadedResponder : IResponder if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) { - return Result.FromSuccess(); + return Result.Success; } if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index 05ceb2f..61ef5cc 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -54,7 +54,7 @@ public class GuildMemberJoinedResponder : IResponder if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); @@ -85,7 +85,7 @@ public class GuildMemberJoinedResponder : IResponder { if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { - return Result.FromSuccess(); + return Result.Success; } var assignRoles = new List(); diff --git a/src/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs index b029c96..90cc64c 100644 --- a/src/Responders/GuildMemberLeftResponder.cs +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -38,13 +38,13 @@ public class GuildMemberLeftResponder : IResponder var memberData = data.GetOrCreateMemberData(user.ID); if (memberData.BannedUntil is not null || memberData.Kicked) { - return Result.FromSuccess(); + return Result.Success; } if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") { - return Result.FromSuccess(); + return Result.Success; } Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Responders/GuildUnloadedResponder.cs b/src/Responders/GuildUnloadedResponder.cs index 47bde75..b49d136 100644 --- a/src/Responders/GuildUnloadedResponder.cs +++ b/src/Responders/GuildUnloadedResponder.cs @@ -33,6 +33,6 @@ public class GuildUnloadedResponder : IResponder _logger.LogInformation("Unloaded guild {GuildId}", guildId); } - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 7d788a7..5a69273 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -39,13 +39,13 @@ public class MessageDeletedResponder : IResponder { if (!gatewayEvent.GuildID.IsDefined(out var guildId)) { - return Result.FromSuccess(); + return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); @@ -56,7 +56,7 @@ public class MessageDeletedResponder : IResponder if (string.IsNullOrWhiteSpace(message.Content)) { - return Result.FromSuccess(); + return Result.Success; } var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index dd3f51d..2f30732 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -48,28 +48,28 @@ public class MessageEditedResponder : IResponder if (!gatewayEvent.GuildID.IsDefined(out var guildId)) { - return Result.FromSuccess(); + return Result.Success; } if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false)) { - return Result.FromSuccess(); + return Result.Success; } if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) { - return Result.FromSuccess(); // The message wasn't actually edited + return Result.Success; // The message wasn't actually edited } if (!gatewayEvent.Content.IsDefined(out var newContent)) { - return Result.FromSuccess(); + return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); @@ -78,12 +78,12 @@ public class MessageEditedResponder : IResponder if (!messageResult.IsDefined(out var message)) { _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - return Result.FromSuccess(); + return Result.Success; } if (message.Content == newContent) { - return Result.FromSuccess(); + return Result.Success; } // Custom event responders are called earlier than responders responsible for message caching diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index 6ab7199..4c26d8d 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -34,6 +34,6 @@ public class MessageCreateResponder : IResponder "лан" => "https://i.ibb.co/VYH2QLc/lan.jpg", _ => default(Optional) }); - return Task.FromResult(Result.FromSuccess()); + return Task.FromResult(Result.Success); } } diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index a21cdd4..45d0476 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -121,7 +121,7 @@ public sealed partial class MemberUpdateService : BackgroundService if (!canInteract) { - return Result.FromSuccess(); + return Result.Success; } var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); @@ -148,14 +148,14 @@ public sealed partial class MemberUpdateService : BackgroundService { if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { - return Result.FromSuccess(); + return Result.Success; } var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct); if (!existingBanResult.IsDefined()) { data.BannedUntil = null; - return Result.FromSuccess(); + return Result.Success; } var unbanResult = await _guildApi.RemoveGuildBanAsync( @@ -173,7 +173,7 @@ public sealed partial class MemberUpdateService : BackgroundService { if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { - return Result.FromSuccess(); + return Result.Success; } var unmuteResult = await _guildApi.ModifyGuildMemberAsync( @@ -209,7 +209,7 @@ public sealed partial class MemberUpdateService : BackgroundService if (!usernameChanged) { - return Result.FromSuccess(); + return Result.Success; } var newNickname = string.Concat(characterList.ToArray()); @@ -230,12 +230,13 @@ public sealed partial class MemberUpdateService : BackgroundService { if (DateTimeOffset.UtcNow < reminder.At) { - return Result.FromSuccess(); + return Result.Success; } var builder = new StringBuilder() .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) - .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); + .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, + $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.Reminder, user.GetTag()), user) @@ -251,6 +252,6 @@ public sealed partial class MemberUpdateService : BackgroundService } data.Reminders.Remove(reminder); - return Result.FromSuccess(); + return Result.Success; } } diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs index bce9996..8168fc1 100644 --- a/src/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -147,7 +147,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService || eventData.EarlyNotificationSent || DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset) { - return Result.FromSuccess(); + return Result.Success; } var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); @@ -182,7 +182,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (GuildSettings.EventNotificationChannel.Get(settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } if (!scheduledEvent.Creator.IsDefined(out var creator)) @@ -283,7 +283,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var embedDescriptionResult = scheduledEvent.EntityType switch @@ -324,7 +324,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { data.ScheduledEvents.Remove(eventData.Id); - return Result.FromSuccess(); + return Result.Success; } var completedEmbed = new EmbedBuilder() @@ -356,7 +356,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { data.ScheduledEvents.Remove(eventData.Id); - return Result.FromSuccess(); + return Result.Success; } var embed = new EmbedBuilder() @@ -409,7 +409,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { - return Result.FromSuccess(); + return Result.Success; } var contentResult = await _utility.GetEventNotificationMentions( From e0232f600810e0dd1c347bb77856292f5273f3ca Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 21 Mar 2024 21:31:17 +0500 Subject: [PATCH 13/20] Merge some sequential 'if' statements (#284) i thought there would be a lot of statements that could be merged, but these are only ones I could find, apparently Signed-off-by: Octol1ttle --- src/Responders/GuildLoadedResponder.cs | 8 ++------ src/Responders/MessageEditedResponder.cs | 22 +++++----------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs index cd40134..b03fd3f 100644 --- a/src/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -74,12 +74,8 @@ public class GuildLoadedResponder : IResponder _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members", guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount); - if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) - { - return Result.Success; - } - - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() + || !GuildSettings.ReceiveStartupMessages.Get(cfg)) { return Result.Success; } diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs index 2f30732..1143652 100644 --- a/src/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -46,28 +46,16 @@ public class MessageEditedResponder : IResponder return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); } - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) - { - return Result.Success; - } - - if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false)) - { - return Result.Success; - } - - if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) - { - return Result.Success; // The message wasn't actually edited - } - - if (!gatewayEvent.Content.IsDefined(out var newContent)) + if (!gatewayEvent.GuildID.IsDefined(out var guildId) + || !gatewayEvent.Author.IsDefined(out var author) + || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp) + || !gatewayEvent.Content.IsDefined(out var newContent)) { return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { return Result.Success; } From ac8621a2ec8e54c96fa04020756914c11132caa7 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:45:39 +0300 Subject: [PATCH 14/20] Pre-Wiki Update (#285) This PR has been opened to finally update Octobot's Wiki. Current changes summary: - correct minor spelling issues in some command descriptions - /about: add Octobot's Wiki button --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ locale/Messages.tt-ru.resx | 3 +++ src/BuildInfo.cs | 2 ++ src/Commands/AboutCommandGroup.cs | 9 ++++++++- src/Commands/SettingsCommandGroup.cs | 2 +- src/Commands/ToolsCommandGroup.cs | 4 ++-- src/Messages.Designer.cs | 6 ++++++ 8 files changed, 28 insertions(+), 4 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index f7500b6..41bb6ef 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -672,4 +672,7 @@ Can't report an issue in the development version + + Open Octobot's Wiki + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index e28b405..273338b 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -672,4 +672,7 @@ Нельзя сообщить о проблеме в версии под разработкой + + Открыть Octobot's Wiki + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 58e1178..af2c94d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -672,4 +672,7 @@ вот иди сам и почини что сломал + + вики Octobot (жмак) + diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index 369feff..fc3a089 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -6,6 +6,8 @@ public static class BuildInfo public static string IssuesUrl => $"{RepositoryUrl}/issues"; + public static string WikiUrl => $"{RepositoryUrl}/wiki"; + private static string Commit => ThisAssembly.Git.Commit; private static string Branch => ThisAssembly.Git.Branch; diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index b37b2f0..027e7f8 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -111,6 +111,13 @@ public class AboutCommandGroup : CommandGroup URL: BuildInfo.RepositoryUrl ); + var wikiButton = new ButtonComponent( + ButtonComponentStyle.Link, + Messages.ButtonOpenWiki, + new PartialEmoji(Name: "📖"), + URL: BuildInfo.WikiUrl + ); + var issuesButton = new ButtonComponent( ButtonComponentStyle.Link, BuildInfo.IsDirty @@ -124,7 +131,7 @@ public class AboutCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { repositoryButton, issuesButton }) + new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton }) }), ct); } } diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 30150ee..f756e93 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -241,7 +241,7 @@ public class SettingsCommandGroup : CommandGroup [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)] - [Description("Reset settings for this server")] + [Description("Reset settings for this guild")] [UsedImplicitly] public async Task ExecuteResetSettingsAsync( [Description("Setting to reset")] AllOptionsEnum? setting = null) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index cc3c2cf..d4f3f75 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -262,7 +262,7 @@ public class ToolsCommandGroup : CommandGroup /// [Command("guildinfo")] [DiscordDefaultDMPermission(false)] - [Description("Shows info current guild")] + [Description("Shows info about current guild")] [UsedImplicitly] public async Task ExecuteGuildInfoAsync() { @@ -514,7 +514,7 @@ public class ToolsCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteEightBallAsync( // let the user think he's actually asking the ball a question - string question) + [Description("Question to ask")] string question) { if (!_context.TryGetContextIDs(out var guildId, out _, out _)) { diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 3d452a6..2910bae 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1210,5 +1210,11 @@ namespace Octobot { return ResourceManager.GetString("ButtonDirty", resourceCulture); } } + + internal static string ButtonOpenWiki { + get { + return ResourceManager.GetString("ButtonOpenWiki", resourceCulture); + } + } } } From a9509deb1cd3dc9b02127d8d27520010c0c97d2d Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:11:00 +0300 Subject: [PATCH 15/20] Don't use BannedUntil in MemberData. (#286) Signed-off-by: mctaylors --- src/Data/MemberData.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 8e23e54..1b3d0d9 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,10 +5,9 @@ namespace Octobot.Data; /// public sealed class MemberData { - public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List? reminders = null) + public MemberData(ulong id, List? reminders = null) { Id = id; - BannedUntil = bannedUntil; if (reminders is not null) { Reminders = reminders; From 844615e8bfac768e1b7bdf3165eb0db045ed5d8c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 17:39:26 +0500 Subject: [PATCH 16/20] fix: do not use RepositoryUrl from GitInfo (#287) GitInfo's `RepositoryUrl` string depends on origin URL, which is unvalidated user input that isn't even guaranteed to exist. This can cause issues that are almost impossible to debug Closes #281 --- src/BuildInfo.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BuildInfo.cs b/src/BuildInfo.cs index fc3a089..2eb6059 100644 --- a/src/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -2,15 +2,15 @@ public static class BuildInfo { - public static string RepositoryUrl => ThisAssembly.Git.RepositoryUrl; + public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; - public static string IssuesUrl => $"{RepositoryUrl}/issues"; + public const string IssuesUrl = $"{RepositoryUrl}/issues"; - public static string WikiUrl => $"{RepositoryUrl}/wiki"; + public const string WikiUrl = $"{RepositoryUrl}/wiki"; - private static string Commit => ThisAssembly.Git.Commit; + private const string Commit = ThisAssembly.Git.Commit; - private static string Branch => ThisAssembly.Git.Branch; + private const string Branch = ThisAssembly.Git.Branch; public static bool IsDirty => ThisAssembly.Git.IsDirty; From c2f7aadaeacdd1f0e88b6bc441f7783cac00ab07 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 20:38:51 +0500 Subject: [PATCH 17/20] Do not use ResultError#IsUserOrEnvironmentError (#289) In LoggerExtensions#LogResult we skip logging the result if the error is "user or environment error". What matches that criteria is defined by Remora's implementation. However, none of errors defined by the implementation should *ever* happen or be ignored: * CommandNotFoundError: The client shouldn't send us non-existing commands. This *can* happen because the client's command list can get out of sync with the server's, but this happens rarely. * AmbiguousCommandInvocationError: We don't have commands that would trigger this error * RequiredParameterValueMissingError: The client shouldn't send us commands without required paremeters * ParameterParsingError: See #220 * ConditionNotSatisfiedError: The client shouldn't send us commands that don't satisfy our conditions Closes #220 --- src/Extensions/LoggerExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs index 9df90b8..fca3702 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Remora.Discord.Commands.Extensions; using Remora.Results; namespace Octobot.Extensions; @@ -19,7 +18,7 @@ public static class LoggerExtensions /// The message to use if this result has failed. public static void LogResult(this ILogger logger, IResult result, string? message = "") { - if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) + if (result.IsSuccess) { return; } From 5e4d0a528c5a604511a02a6f9a06d2d77378b0f4 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 20:48:32 +0500 Subject: [PATCH 18/20] Split message clear log when cleared messages are too long (#288) This change makes Octobot split the message clear log into multiple messages when the combined length of cleared messages exceeds the maximum length for an embed description. Closes #180 --- src/Commands/ClearCommandGroup.cs | 34 +++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 70afede..84b69db 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -102,7 +102,9 @@ public class ClearCommandGroup : CommandGroup CancellationToken ct = default) { var idList = new List(messages.Count); - var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); + + var logEntries = new List { new() }; + var currentLogEntry = 0; for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') { var message = messages[i]; @@ -112,8 +114,17 @@ public class ClearCommandGroup : CommandGroup } idList.Add(message.ID); - builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author))); - builder.Append(message.Content.InBlockCode()); + + var entry = logEntries[currentLogEntry]; + var str = $"{string.Format(Messages.MessageFrom, Mention.User(message.Author))}\n{message.Content.InBlockCode()}"; + if (entry.Builder.Length + str.Length > EmbedConstants.MaxDescriptionLength) + { + logEntries.Add(entry = new ClearedMessageEntry()); + currentLogEntry++; + } + + entry.Builder.Append(str); + entry.DeletedCount++; } if (idList.Count == 0) @@ -127,7 +138,6 @@ public class ClearCommandGroup : CommandGroup 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( channelId, idList, executor.GetTag().EncodeHeader(), ct); @@ -136,12 +146,24 @@ public class ClearCommandGroup : CommandGroup return ResultExtensions.FromError(deleteResult); } - _utility.LogAction( - data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); + foreach (var log in logEntries) + { + _utility.LogAction( + data.Settings, channelId, executor, author is not null + ? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag()) + : string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()), + log.Builder.ToString(), bot, ColorsList.Red, false, ct); + } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + private sealed class ClearedMessageEntry + { + public StringBuilder Builder { get; } = new(); + public int DeletedCount { get; set; } + } } From 171cfaea1ac2789a189fc8a7da0fede6d3e727cc Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 23:29:10 +0500 Subject: [PATCH 19/20] Add 'ModeratorRole' guild setting (#290) Octobot has various moderation commands such as /ban, /mute, /kick. These commands add multiple features to Discord's built-in functions (such as durations and logging). Some admins may want to force their users to use Octobot's commands instead of Discord UI functions. However, due to the current design, they can't take away the permissions as that remove access to the respective command. This PR adds the `ModeratorRole` option which allows anyone who has `ManageMessages` permission and the role to perform any moderator action. If the role is not set, the Discord permissions are checked instead. If the user doesn't have the role, but has the permission, they can still run the command. --------- Signed-off-by: Octol1ttle --- locale/Messages.resx | 10 +- locale/Messages.ru.resx | 10 +- locale/Messages.tt-ru.resx | 10 +- src/Commands/BanCommandGroup.cs | 32 ++-- src/Commands/KickCommandGroup.cs | 22 +-- src/Commands/MuteCommandGroup.cs | 21 +-- src/Commands/SettingsCommandGroup.cs | 1 + src/Data/GuildSettings.cs | 1 + src/Data/Options/AllOptionsEnum.cs | 1 + src/Octobot.cs | 3 +- src/Services/AccessControlService.cs | 176 +++++++++++++++++++++ src/Services/Update/MemberUpdateService.cs | 10 +- src/Services/Utility.cs | 118 +------------- 13 files changed, 252 insertions(+), 163 deletions(-) create mode 100644 src/Services/AccessControlService.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 41bb6ef..47e7d4f 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -231,8 +231,11 @@ You cannot kick members from this guild! - - You cannot moderate members in this guild! + + You cannot mute members in this guild! + + + You cannot unmute members in this guild! You cannot manage this guild! @@ -675,4 +678,7 @@ Open Octobot's Wiki + + Moderator role + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 273338b..2eef257 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -228,8 +228,11 @@ Ты не можешь выгонять участников с этого сервера! - - Ты не можешь модерировать участников этого сервера! + + Ты не можешь глушить участников этого сервера! + + + Ты не можешь разглушать участников этого сервера! Ты не можешь настраивать этот сервер! @@ -675,4 +678,7 @@ Открыть Octobot's Wiki + + Роль модератора + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index af2c94d..4e92a44 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -231,8 +231,11 @@ кик шизиков нельзя - - тебе нельзя управлять шизоидами + + тебе нельзя мутить шизоидов + + + тебе нельзя раззамучивать шизоидов тебе нельзя редактировать дурку @@ -675,4 +678,7 @@ вики Octobot (жмак) + + звание админа + diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index ef5e9a4..02a377a 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -28,6 +28,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class BanCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -36,16 +37,16 @@ public class BanCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public BanCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - Utility utility) + public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData, + IDiscordRestUserAPI userApi, Utility utility) { - _context = context; + _access = access; _channelApi = channelApi; - _guildData = guildData; + _context = context; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -65,10 +66,10 @@ public class BanCommandGroup : CommandGroup /// /// [Command("ban", "бан")] - [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Ban user")] [UsedImplicitly] @@ -128,7 +129,8 @@ public class BanCommandGroup : CommandGroup } private async Task BanUserAsync( - IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, + IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, + Snowflake channelId, IUser bot, CancellationToken ct = default) { var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); @@ -141,7 +143,7 @@ public class BanCommandGroup : CommandGroup } var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -155,7 +157,8 @@ public class BanCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } - var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); + var builder = + new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); if (duration is not null) { builder.AppendBulletPoint( @@ -221,10 +224,10 @@ public class BanCommandGroup : CommandGroup /// /// [Command("unban")] - [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("Unban user")] [UsedImplicitly] @@ -286,7 +289,8 @@ public class BanCommandGroup : CommandGroup .WithColour(ColorsList.Green).Build(); var title = string.Format(Messages.UserUnbanned, target.GetTag()); - var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); + var description = + new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); _utility.LogAction( data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 5149ad4..87b915a 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -24,6 +24,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class KickCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -32,16 +33,16 @@ public class KickCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public KickCommandGroup( - ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, - IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - Utility utility) + public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData, + IDiscordRestUserAPI userApi, Utility utility) { - _context = context; + _access = access; _channelApi = channelApi; - _guildData = guildData; + _context = context; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -59,10 +60,10 @@ public class KickCommandGroup : CommandGroup /// was kicked and vice-versa. /// [Command("kick", "кик")] - [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Kick member")] [UsedImplicitly] @@ -115,7 +116,7 @@ public class KickCommandGroup : CommandGroup CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -134,7 +135,8 @@ public class KickCommandGroup : CommandGroup { var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) .WithTitle(Messages.YouWereKicked) - .WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) + .WithDescription( + MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) .WithActionFooter(executor) .WithCurrentTimestamp() .WithColour(ColorsList.Red) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 8e79830..ce0a296 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -28,6 +28,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class MuteCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; private readonly IDiscordRestGuildAPI _guildApi; @@ -35,14 +36,14 @@ public class MuteCommandGroup : CommandGroup private readonly IDiscordRestUserAPI _userApi; private readonly Utility _utility; - public MuteCommandGroup( - ICommandContext context, GuildDataService guildData, IFeedbackService feedback, - IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility) + public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback, + IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility) { + _access = access; _context = context; - _guildData = guildData; _feedback = feedback; _guildApi = guildApi; + _guildData = guildData; _userApi = userApi; _utility = utility; } @@ -62,10 +63,10 @@ public class MuteCommandGroup : CommandGroup /// /// [Command("mute", "мут")] - [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Mute member")] [UsedImplicitly] @@ -127,7 +128,7 @@ public class MuteCommandGroup : CommandGroup Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync( + = await _access.CheckInteractionsAsync( guildId, executor.ID, target.ID, "Mute", ct); if (!interactionResult.IsSuccess) { @@ -239,10 +240,10 @@ public class MuteCommandGroup : CommandGroup /// /// [Command("unmute", "размут")] - [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.ModerateMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [Description("Unmute member")] [UsedImplicitly] @@ -290,7 +291,7 @@ public class MuteCommandGroup : CommandGroup IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync( + = await _access.CheckInteractionsAsync( guildId, executor.ID, target.ID, "Unmute", ct); if (!interactionResult.IsSuccess) { diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index f756e93..a39e9c7 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -51,6 +51,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.EventNotificationChannel, GuildSettings.DefaultRole, GuildSettings.MuteRole, + GuildSettings.ModeratorRole, GuildSettings.EventNotificationRole, GuildSettings.EventEarlyNotificationOffset ]; diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 518465b..a1d8d74 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -76,6 +76,7 @@ public static class GuildSettings public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole"); + public static readonly SnowflakeOption ModeratorRole = new("ModeratorRole"); public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); /// diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index 6932822..d9e0c13 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -26,6 +26,7 @@ public enum AllOptionsEnum [UsedImplicitly] EventNotificationChannel, [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, + [UsedImplicitly] ModeratorRole, [UsedImplicitly] EventNotificationRole, [UsedImplicitly] EventEarlyNotificationOffset } diff --git a/src/Octobot.cs b/src/Octobot.cs index a4871f4..065967e 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -88,8 +88,9 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services - .AddSingleton() + .AddSingleton() .AddSingleton() + .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) .AddHostedService() .AddHostedService() diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs new file mode 100644 index 0000000..84667c3 --- /dev/null +++ b/src/Services/AccessControlService.cs @@ -0,0 +1,176 @@ +using Octobot.Data; +using Octobot.Extensions; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Results; +using Remora.Rest.Core; +using Remora.Results; + +namespace Octobot.Services; + +public sealed class AccessControlService +{ + private readonly GuildDataService _data; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly RequireDiscordPermissionCondition _permission; + private readonly IDiscordRestUserAPI _userApi; + + public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + RequireDiscordPermissionCondition permission) + { + _data = data; + _guildApi = guildApi; + _userApi = userApi; + _permission = permission; + } + + private async Task> CheckPermissionAsync(GuildData data, Snowflake memberId, IGuildMember member, + DiscordPermission permission, CancellationToken ct = default) + { + var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings); + var result = await _permission.CheckAsync(new RequireDiscordPermissionAttribute([permission]), member, ct); + + if (result.Error is not null and not PermissionDeniedError) + { + return Result.FromError(result); + } + + var hasPermission = result.IsSuccess; + return hasPermission || (!moderatorRole.Empty() && + data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)); + } + + /// + /// Checks whether or not a member can interact with another member + /// + /// The ID of the guild in which an operation is being performed. + /// The executor of the operation. + /// The target of the operation. + /// The operation. + /// The cancellation token for this operation. + /// + /// + /// A result which has succeeded with a null string if the member can interact with the target. + /// + /// A result which has succeeded with a non-null string containing the error message if the member cannot + /// interact with the target. + /// + /// A result which has failed if an error occurred during the execution of this method. + /// + /// + public async Task> CheckInteractionsAsync( + Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default) + { + if (interacterId == targetId) + { + return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); + } + + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) + { + return Result.FromSuccess(null); + } + + var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); + if (!currentMemberResult.IsDefined(out var currentMember)) + { + return Result.FromError(currentMemberResult); + } + + var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); + if (!rolesResult.IsDefined(out var roles)) + { + return Result.FromError(rolesResult); + } + + if (interacterId is null) + { + return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); + } + + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); + if (!interacterResult.IsDefined(out var interacter)) + { + return Result.FromError(interacterResult); + } + + var data = await _data.GetData(guildId, ct); + + var permissionResult = await CheckPermissionAsync(data, interacterId.Value, interacter, + action switch + { + "Ban" => DiscordPermission.BanMembers, + "Kick" => DiscordPermission.KickMembers, + "Mute" or "Unmute" => DiscordPermission.ModerateMembers, + _ => throw new Exception() + }, ct); + if (!permissionResult.IsDefined(out var hasPermission)) + { + return Result.FromError(permissionResult); + } + + return hasPermission + ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) + : Result.FromSuccess($"UserCannot{action}Members".Localized()); + } + + private static Result CheckInteractions( + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, + IGuildMember interacter) + { + if (!targetMember.User.IsDefined(out var targetUser)) + { + return new ArgumentNullError(nameof(targetMember.User)); + } + + if (!interacter.User.IsDefined(out var interacterUser)) + { + return new ArgumentNullError(nameof(interacter.User)); + } + + if (currentMember.User == targetMember.User) + { + return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + } + + if (targetUser.ID == guild.OwnerID) + { + return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + } + + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); + var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + + var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); + if (targetBotRoleDiff >= 0) + { + return Result.FromSuccess($"BotCannot{action}Target".Localized()); + } + + if (interacterUser.ID == guild.OwnerID) + { + return Result.FromSuccess(null); + } + + var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); + var targetInteracterRoleDiff + = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); + return targetInteracterRoleDiff < 0 + ? Result.FromSuccess(null) + : Result.FromSuccess($"UserCannot{action}Target".Localized()); + } +} diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 45d0476..e177fca 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -26,20 +26,20 @@ public sealed partial class MemberUpdateService : BackgroundService "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" ]; + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly ILogger _logger; - private readonly Utility _utility; - public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, - GuildDataService guildData, ILogger logger, Utility utility) + public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi, + IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger logger) { + _access = access; _channelApi = channelApi; _guildApi = guildApi; _guildData = guildData; _logger = logger; - _utility = utility; } protected override async Task ExecuteAsync(CancellationToken ct) @@ -94,7 +94,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var interactionResult - = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); + = await _access.CheckInteractionsAsync(guildId, null, id, "Update", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); diff --git a/src/Services/Utility.cs b/src/Services/Utility.cs index ad06315..3b9ab19 100644 --- a/src/Services/Utility.cs +++ b/src/Services/Utility.cs @@ -21,129 +21,13 @@ public sealed class Utility private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; - private readonly IDiscordRestUserAPI _userApi; public Utility( - IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, - IDiscordRestUserAPI userApi) + IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi) { _channelApi = channelApi; _eventApi = eventApi; _guildApi = guildApi; - _userApi = userApi; - } - - /// - /// Checks whether or not a member can interact with another member - /// - /// The ID of the guild in which an operation is being performed. - /// The executor of the operation. - /// The target of the operation. - /// The operation. - /// The cancellation token for this operation. - /// - /// - /// A result which has succeeded with a null string if the member can interact with the target. - /// - /// A result which has succeeded with a non-null string containing the error message if the member cannot - /// interact with the target. - /// - /// A result which has failed if an error occurred during the execution of this method. - /// - /// - public async Task> CheckInteractionsAsync( - Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default) - { - if (interacterId == targetId) - { - return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); - } - - var botResult = await _userApi.GetCurrentUserAsync(ct); - if (!botResult.IsDefined(out var bot)) - { - return Result.FromError(botResult); - } - - var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); - if (!guildResult.IsDefined(out var guild)) - { - return Result.FromError(guildResult); - } - - var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); - if (!targetMemberResult.IsDefined(out var targetMember)) - { - return Result.FromSuccess(null); - } - - var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); - if (!currentMemberResult.IsDefined(out var currentMember)) - { - return Result.FromError(currentMemberResult); - } - - var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); - if (!rolesResult.IsDefined(out var roles)) - { - return Result.FromError(rolesResult); - } - - if (interacterId is null) - { - return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); - } - - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); - return interacterResult.IsDefined(out var interacter) - ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) - : Result.FromError(interacterResult); - } - - private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, - IGuildMember interacter) - { - if (!targetMember.User.IsDefined(out var targetUser)) - { - return new ArgumentNullError(nameof(targetMember.User)); - } - - if (!interacter.User.IsDefined(out var interacterUser)) - { - return new ArgumentNullError(nameof(interacter.User)); - } - - if (currentMember.User == targetMember.User) - { - return Result.FromSuccess($"UserCannot{action}Bot".Localized()); - } - - if (targetUser.ID == guild.OwnerID) - { - return Result.FromSuccess($"UserCannot{action}Owner".Localized()); - } - - var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); - var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); - - var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); - if (targetBotRoleDiff >= 0) - { - return Result.FromSuccess($"BotCannot{action}Target".Localized()); - } - - if (interacterUser.ID == guild.OwnerID) - { - return Result.FromSuccess(null); - } - - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); - var targetInteracterRoleDiff - = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); - return targetInteracterRoleDiff < 0 - ? Result.FromSuccess(null) - : Result.FromSuccess($"UserCannot{action}Target".Localized()); } /// From e76fccd62228190ee528233cc6eb3ab34a2bf1ef Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:40:04 +0300 Subject: [PATCH 20/20] Rename currentMember to botMember (#291) Signed-off-by: mctaylors --- src/Services/AccessControlService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs index 84667c3..aeb16e4 100644 --- a/src/Services/AccessControlService.cs +++ b/src/Services/AccessControlService.cs @@ -85,10 +85,10 @@ public sealed class AccessControlService return Result.FromSuccess(null); } - var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); - if (!currentMemberResult.IsDefined(out var currentMember)) + var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); + if (!botMemberResult.IsDefined(out var botMember)) { - return Result.FromError(currentMemberResult); + return Result.FromError(botMemberResult); } var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); @@ -99,7 +99,7 @@ public sealed class AccessControlService if (interacterId is null) { - return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember); + return CheckInteractions(action, guild, roles, targetMember, botMember, botMember); } var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); @@ -124,12 +124,12 @@ public sealed class AccessControlService } return hasPermission - ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter) + ? CheckInteractions(action, guild, roles, targetMember, botMember, interacter) : Result.FromSuccess($"UserCannot{action}Members".Localized()); } private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember, + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember botMember, IGuildMember interacter) { if (!targetMember.User.IsDefined(out var targetUser)) @@ -142,7 +142,7 @@ public sealed class AccessControlService return new ArgumentNullError(nameof(interacter.User)); } - if (currentMember.User == targetMember.User) + if (botMember.User == targetMember.User) { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); } @@ -153,7 +153,7 @@ public sealed class AccessControlService } var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); - var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + var botRoles = roles.Where(r => botMember.Roles.Contains(r.ID)); var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); if (targetBotRoleDiff >= 0)