From 844615e8bfac768e1b7bdf3165eb0db045ed5d8c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 24 Mar 2024 17:39:26 +0500 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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) From cccc4d6205259d2712567e2de91174112395cc2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:47:05 +0300 Subject: [PATCH 6/6] Bump muno92/resharper_inspectcode from 1.11.7 to 1.11.8 (#292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [muno92/resharper_inspectcode](https://github.com/muno92/resharper_inspectcode) from 1.11.7 to 1.11.8.
Release notes

Sourced from muno92/resharper_inspectcode's releases.

1.11.8

What's Changed

New Contributors

Full Changelog: https://github.com/muno92/resharper_inspectcode/compare/1.11.7...1.11.8

Changelog

Sourced from muno92/resharper_inspectcode's changelog.

1.11.8 - 2024-03-23

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=muno92/resharper_inspectcode&package-manager=github_actions&previous-version=1.11.7&new-version=1.11.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 8002f6f..859f8fa 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.7 + uses: muno92/resharper_inspectcode@1.11.8 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor