mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-18 16:03:36 +03:00
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 <l1ttleofficial@outlook.com>
174 lines
6.8 KiB
C#
174 lines
6.8 KiB
C#
using System.ComponentModel;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using JetBrains.Annotations;
|
|
using Octobot.Data;
|
|
using Octobot.Extensions;
|
|
using Octobot.Services;
|
|
using Remora.Commands.Attributes;
|
|
using Remora.Commands.Groups;
|
|
using Remora.Discord.API.Abstractions.Objects;
|
|
using Remora.Discord.API.Abstractions.Rest;
|
|
using Remora.Discord.Commands.Attributes;
|
|
using Remora.Discord.Commands.Conditions;
|
|
using Remora.Discord.Commands.Contexts;
|
|
using Remora.Discord.Commands.Feedback.Services;
|
|
using Remora.Discord.Extensions.Embeds;
|
|
using Remora.Rest.Core;
|
|
using Remora.Results;
|
|
|
|
namespace Octobot.Commands;
|
|
|
|
/// <summary>
|
|
/// Handles the command to kick members of a guild: /kick.
|
|
/// </summary>
|
|
[UsedImplicitly]
|
|
public class KickCommandGroup : CommandGroup
|
|
{
|
|
private readonly AccessControlService _access;
|
|
private readonly IDiscordRestChannelAPI _channelApi;
|
|
private readonly ICommandContext _context;
|
|
private readonly IFeedbackService _feedback;
|
|
private readonly IDiscordRestGuildAPI _guildApi;
|
|
private readonly GuildDataService _guildData;
|
|
private readonly IDiscordRestUserAPI _userApi;
|
|
private readonly Utility _utility;
|
|
|
|
public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
|
|
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
|
|
IDiscordRestUserAPI userApi, Utility utility)
|
|
{
|
|
_access = access;
|
|
_channelApi = channelApi;
|
|
_context = context;
|
|
_feedback = feedback;
|
|
_guildApi = guildApi;
|
|
_guildData = guildData;
|
|
_userApi = userApi;
|
|
_utility = utility;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A slash command that kicks a Discord member with the specified reason.
|
|
/// </summary>
|
|
/// <param name="target">The member to kick.</param>
|
|
/// <param name="reason">
|
|
/// The reason for this kick. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
|
/// <see cref="IDiscordRestGuildAPI.RemoveGuildMemberAsync" />.
|
|
/// </param>
|
|
/// <returns>
|
|
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
|
|
/// was kicked and vice-versa.
|
|
/// </returns>
|
|
[Command("kick", "кик")]
|
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
|
[DiscordDefaultDMPermission(false)]
|
|
[RequireContext(ChannelContext.Guild)]
|
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
|
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
|
[Description("Kick member")]
|
|
[UsedImplicitly]
|
|
public async Task<Result> ExecuteKick(
|
|
[Description("Member to kick")] IUser target,
|
|
[Description("Kick reason")] [MaxLength(256)]
|
|
string reason)
|
|
{
|
|
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
|
{
|
|
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
|
}
|
|
|
|
// The bot's avatar is used when sending error messages
|
|
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
|
if (!botResult.IsDefined(out var bot))
|
|
{
|
|
return ResultExtensions.FromError(botResult);
|
|
}
|
|
|
|
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
|
if (!executorResult.IsDefined(out var executor))
|
|
{
|
|
return ResultExtensions.FromError(executorResult);
|
|
}
|
|
|
|
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
|
if (!guildResult.IsDefined(out var guild))
|
|
{
|
|
return ResultExtensions.FromError(guildResult);
|
|
}
|
|
|
|
var data = await _guildData.GetData(guildId, CancellationToken);
|
|
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
|
|
|
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken);
|
|
if (!memberResult.IsSuccess)
|
|
{
|
|
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot)
|
|
.WithColour(ColorsList.Red).Build();
|
|
|
|
return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken);
|
|
}
|
|
|
|
return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken);
|
|
}
|
|
|
|
private async Task<Result> KickUserAsync(
|
|
IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot,
|
|
CancellationToken ct = default)
|
|
{
|
|
var interactionResult
|
|
= await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct);
|
|
if (!interactionResult.IsSuccess)
|
|
{
|
|
return ResultExtensions.FromError(interactionResult);
|
|
}
|
|
|
|
if (interactionResult.Entity is not null)
|
|
{
|
|
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
|
|
.WithColour(ColorsList.Red).Build();
|
|
|
|
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
|
}
|
|
|
|
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
|
|
if (dmChannelResult.IsDefined(out var dmChannel))
|
|
{
|
|
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
|
|
.WithTitle(Messages.YouWereKicked)
|
|
.WithDescription(
|
|
MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
|
|
.WithActionFooter(executor)
|
|
.WithCurrentTimestamp()
|
|
.WithColour(ColorsList.Red)
|
|
.Build();
|
|
|
|
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 ResultExtensions.FromError(kickResult);
|
|
}
|
|
|
|
memberData.Roles.Clear();
|
|
|
|
var title = string.Format(Messages.UserKicked, target.GetTag());
|
|
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
|
|
|
_utility.LogAction(
|
|
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
|
|
|
var embed = new EmbedBuilder().WithSmallTitle(
|
|
string.Format(Messages.UserKicked, target.GetTag()), target)
|
|
.WithColour(ColorsList.Green).Build();
|
|
|
|
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
|
}
|
|
}
|