diff --git a/.github/workflows/resharper.yml b/.github/workflows/resharper.yml index b0ec1a3..29302aa 100644 --- a/.github/workflows/resharper.yml +++ b/.github/workflows/resharper.yml @@ -31,5 +31,5 @@ jobs: uses: muno92/resharper_inspectcode@1.7.1 with: solutionPath: ./Boyfriend.sln - ignoreIssueType: InvertIf, ConvertIfStatementToReturnStatement, ConvertIfStatementToSwitchStatement + ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement solutionWideAnalysis: true diff --git a/docs/README.md b/docs/README.md index 0cde8ef..e6d35fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,6 @@ ![GitHub License](https://img.shields.io/github/license/TeamOctolings/Boyfriend) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/TeamOctolings/Boyfriend/.github/workflows/resharper.yml?branch=master) ![GitHub last commit](https://img.shields.io/github/last-commit/TeamOctolings/Boyfriend) -![CodeFactor](https://img.shields.io/codefactor/grade/github/TeamOctolings/Boyfriend) Beep! I'm a general-purpose bot for moderation written by [@Octol1ttle](https://github.com/Octol1ttle) in C# and Remora.Discord diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index 902c753..14f6d4f 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -152,7 +152,9 @@ public class BanCommandGroup : CommandGroup { title, target) .WithColour(ColorsList.Green).Build(); - _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); } @@ -222,7 +224,7 @@ public class BanCommandGroup : CommandGroup { var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = string.Format(Messages.DescriptionActionReason, reason); - var logResult = _utility.LogActionAsync(cfg, channelId, title, target, description, user, CancellationToken); + var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); if (!logResult.IsSuccess) return Result.FromError(logResult.Error); diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 306ab86..ce1787c 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -28,15 +28,17 @@ public class ClearCommandGroup : CommandGroup { private readonly GuildDataService _dataService; private readonly FeedbackService _feedbackService; private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; public ClearCommandGroup( IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService dataService, - FeedbackService feedbackService, IDiscordRestUserAPI userApi) { + FeedbackService feedbackService, IDiscordRestUserAPI userApi, UtilityService utility) { _channelApi = channelApi; _context = context; _dataService = dataService; _feedbackService = feedbackService; _userApi = userApi; + _utility = utility; } /// @@ -55,7 +57,7 @@ public class ClearCommandGroup : CommandGroup { [RequireBotDiscordPermissions(DiscordPermission.ManageMessages)] [Description("Remove multiple messages")] [UsedImplicitly] - public async Task ClearMessagesAsync( + public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) @@ -66,12 +68,25 @@ public class ClearCommandGroup : CommandGroup { channelId.Value, limit: amount + 1, ct: CancellationToken); if (!messagesResult.IsDefined(out var messages)) return Result.FromError(messagesResult); + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + // The current user's avatar is used when sending messages + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); - var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); + return await ClearMessagesAsync(amount, guildId.Value, channelId.Value, messages, user, currentUser); + } + + private async Task ClearMessagesAsync( + int amount, Snowflake guildId, Snowflake channelId, IReadOnlyList messages, + IUser user, IUser currentUser) { + var cfg = await _dataService.GetSettings(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(cfg); var idList = new List(messages.Count); - var builder = new StringBuilder().AppendLine(Mention.Channel(channelId.Value)).AppendLine(); + var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') var message = messages[i]; idList.Add(message.ID); @@ -79,41 +94,18 @@ public class ClearCommandGroup : CommandGroup { builder.Append(message.Content.InBlockCode()); } + var title = string.Format(Messages.MessagesCleared, amount.ToString()); var description = builder.ToString(); - var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); - if (!userResult.IsDefined(out var user)) - return Result.FromError(userResult); - var deleteResult = await _channelApi.BulkDeleteMessagesAsync( - channelId.Value, idList, user.GetTag().EncodeHeader(), CancellationToken); + channelId, idList, user.GetTag().EncodeHeader(), CancellationToken); if (!deleteResult.IsSuccess) return Result.FromError(deleteResult.Error); - // The current user's avatar is used when sending messages - var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!currentUserResult.IsDefined(out var currentUser)) - return Result.FromError(currentUserResult); - - var title = string.Format(Messages.MessagesCleared, amount.ToString()); - if (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty() - && GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) { - var logEmbed = new EmbedBuilder().WithSmallTitle(title, currentUser) - .WithDescription(description) - .WithActionFooter(user) - .WithCurrentTimestamp() - .WithColour(ColorsList.Red) - .Build(); - - if (!logEmbed.IsDefined(out var logBuilt)) - return Result.FromError(logEmbed); - - // Not awaiting to reduce response time - if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value) - _ = _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { logBuilt }, - ct: CancellationToken); - } + var logResult = _utility.LogActionAsync( + cfg, channelId, user, title, description, currentUser, CancellationToken); + if (!logResult.IsSuccess) + return Result.FromError(logResult.Error); var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) .WithColour(ColorsList.Green).Build(); diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index 6c4a49f..dbe1b4f 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -27,9 +27,8 @@ public class LanguageOption : Option { /// public override Result Set(JsonNode settings, string from) { - if (!CultureInfoCache.ContainsKey(from.ToLowerInvariant())) - return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); - - return base.Set(settings, from.ToLowerInvariant()); + return CultureInfoCache.ContainsKey(from.ToLowerInvariant()) + ? base.Set(settings, from.ToLowerInvariant()) + : Result.FromError(new ArgumentInvalidError(nameof(from), Messages.LanguageNotSupported)); } } diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs index 903db84..652631e 100644 --- a/src/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -46,15 +46,13 @@ public class MessageDeletedResponder : IResponder { if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); var auditLog = auditLogPage.AuditLogEntries.Single(); - if (!auditLog.Options.IsDefined(out var options)) - return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); - var user = message.Author; - if (options.ChannelID == gatewayEvent.ChannelID - && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); - if (!userResult.IsDefined(out user)) return Result.FromError(userResult); - } + var userResult = Result.FromSuccess(message.Author); + if (auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID + && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) + userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + + if (!userResult.IsDefined(out var user)) return Result.FromError(userResult); Messages.Culture = GuildSettings.Language.Get(cfg); diff --git a/src/Services/UtilityService.cs b/src/Services/UtilityService.cs index b1ae347..18808eb 100644 --- a/src/Services/UtilityService.cs +++ b/src/Services/UtilityService.cs @@ -64,13 +64,10 @@ public class UtilityService : IHostedService { var currentUserResult = await _userApi.GetCurrentUserAsync(ct); if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - if (currentUser.ID == targetId) - return Result.FromSuccess($"UserCannot{action}Bot".Localized()); var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - if (targetId == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); if (!targetMemberResult.IsDefined(out var targetMember)) @@ -84,6 +81,25 @@ public class UtilityService : IHostedService { if (!rolesResult.IsDefined(out var roles)) return Result.FromError(rolesResult); + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, 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 Result.FromError(new ArgumentNullError(nameof(targetMember.User))); + if (!interacter.User.IsDefined(out var interacterUser)) + return Result.FromError(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)); @@ -91,20 +107,15 @@ public class UtilityService : IHostedService { if (targetBotRoleDiff >= 0) return Result.FromSuccess($"BotCannot{action}Target".Localized()); - if (interacterId == guild.OwnerID) + if (interacterUser.ID == guild.OwnerID) return Result.FromSuccess(null); - var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); - if (!interacterResult.IsDefined(out var interacter)) - return Result.FromError(interacterResult); - var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); - if (targetInteracterRoleDiff >= 0) - return Result.FromSuccess($"UserCannot{action}Target".Localized()); - - return Result.FromSuccess(null); + return targetInteracterRoleDiff < 0 + ? Result.FromSuccess(null) + : Result.FromSuccess($"UserCannot{action}Target".Localized()); } /// @@ -143,15 +154,15 @@ public class UtilityService : IHostedService { /// /// The guild configuration. /// The ID of the channel where the action was executed. - /// The title for the embed. - /// The user whose avatar will be displayed next to the of the embed. - /// The description of the embed. /// The user who performed the action. + /// The title for the embed. + /// The description of the embed. + /// The user whose avatar will be displayed next to the of the embed. /// The cancellation token for this operation. /// public Result LogActionAsync( - JsonNode cfg, Snowflake channelId, string title, IUser avatar, string description, - IUser user, CancellationToken ct = default) { + JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, + CancellationToken ct = default) { var publicChannel = GuildSettings.PublicFeedbackChannel.Get(cfg); var privateChannel = GuildSettings.PrivateFeedbackChannel.Get(cfg); if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)