mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-01-31 00:19:00 +03:00
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 <l1ttleofficial@outlook.com>
This commit is contained in:
parent
5e4d0a528c
commit
171cfaea1a
13 changed files with 252 additions and 163 deletions
|
@ -231,8 +231,11 @@
|
||||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||||
<value>You cannot kick members from this guild!</value>
|
<value>You cannot kick members from this guild!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||||
<value>You cannot moderate members in this guild!</value>
|
<value>You cannot mute members in this guild!</value>
|
||||||
|
</data>
|
||||||
|
<data name="UserCannotUnmuteMembers" xml:space="preserve">
|
||||||
|
<value>You cannot unmute members in this guild!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotManageGuild" xml:space="preserve">
|
<data name="UserCannotManageGuild" xml:space="preserve">
|
||||||
<value>You cannot manage this guild!</value>
|
<value>You cannot manage this guild!</value>
|
||||||
|
@ -675,4 +678,7 @@
|
||||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||||
<value>Open Octobot's Wiki</value>
|
<value>Open Octobot's Wiki</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||||
|
<value>Moderator role</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -228,8 +228,11 @@
|
||||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||||
<value>Ты не можешь выгонять участников с этого сервера!</value>
|
<value>Ты не можешь выгонять участников с этого сервера!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||||
<value>Ты не можешь модерировать участников этого сервера!</value>
|
<value>Ты не можешь глушить участников этого сервера!</value>
|
||||||
|
</data>
|
||||||
|
<data name="UserCannotUnmuteMembers" xml:space="preserve">
|
||||||
|
<value>Ты не можешь разглушать участников этого сервера!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotManageGuild" xml:space="preserve">
|
<data name="UserCannotManageGuild" xml:space="preserve">
|
||||||
<value>Ты не можешь настраивать этот сервер!</value>
|
<value>Ты не можешь настраивать этот сервер!</value>
|
||||||
|
@ -675,4 +678,7 @@
|
||||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||||
<value>Открыть Octobot's Wiki</value>
|
<value>Открыть Octobot's Wiki</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||||
|
<value>Роль модератора</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -231,8 +231,11 @@
|
||||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||||
<value>кик шизиков нельзя</value>
|
<value>кик шизиков нельзя</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||||
<value>тебе нельзя управлять шизоидами</value>
|
<value>тебе нельзя мутить шизоидов</value>
|
||||||
|
</data>
|
||||||
|
<data name="UserCannotUnmuteMembers" xml:space="preserve">
|
||||||
|
<value>тебе нельзя раззамучивать шизоидов</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UserCannotManageGuild" xml:space="preserve">
|
<data name="UserCannotManageGuild" xml:space="preserve">
|
||||||
<value>тебе нельзя редактировать дурку</value>
|
<value>тебе нельзя редактировать дурку</value>
|
||||||
|
@ -675,4 +678,7 @@
|
||||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||||
<value>вики Octobot (жмак)</value>
|
<value>вики Octobot (жмак)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||||
|
<value>звание админа</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace Octobot.Commands;
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class BanCommandGroup : CommandGroup
|
public class BanCommandGroup : CommandGroup
|
||||||
{
|
{
|
||||||
|
private readonly AccessControlService _access;
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
private readonly IFeedbackService _feedback;
|
private readonly IFeedbackService _feedback;
|
||||||
|
@ -36,16 +37,16 @@ public class BanCommandGroup : CommandGroup
|
||||||
private readonly IDiscordRestUserAPI _userApi;
|
private readonly IDiscordRestUserAPI _userApi;
|
||||||
private readonly Utility _utility;
|
private readonly Utility _utility;
|
||||||
|
|
||||||
public BanCommandGroup(
|
public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
|
||||||
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
|
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
|
||||||
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
|
IDiscordRestUserAPI userApi, Utility utility)
|
||||||
Utility utility)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_access = access;
|
||||||
_channelApi = channelApi;
|
_channelApi = channelApi;
|
||||||
_guildData = guildData;
|
_context = context;
|
||||||
_feedback = feedback;
|
_feedback = feedback;
|
||||||
_guildApi = guildApi;
|
_guildApi = guildApi;
|
||||||
|
_guildData = guildData;
|
||||||
_userApi = userApi;
|
_userApi = userApi;
|
||||||
_utility = utility;
|
_utility = utility;
|
||||||
}
|
}
|
||||||
|
@ -65,10 +66,10 @@ public class BanCommandGroup : CommandGroup
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <seealso cref="ExecuteUnban" />
|
/// <seealso cref="ExecuteUnban" />
|
||||||
[Command("ban", "бан")]
|
[Command("ban", "бан")]
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
[DiscordDefaultDMPermission(false)]
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||||
[Description("Ban user")]
|
[Description("Ban user")]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
|
@ -128,7 +129,8 @@ public class BanCommandGroup : CommandGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result> BanUserAsync(
|
private async Task<Result> 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)
|
IUser bot, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
|
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
|
||||||
|
@ -141,7 +143,7 @@ public class BanCommandGroup : CommandGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
var interactionResult
|
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)
|
if (!interactionResult.IsSuccess)
|
||||||
{
|
{
|
||||||
return ResultExtensions.FromError(interactionResult);
|
return ResultExtensions.FromError(interactionResult);
|
||||||
|
@ -155,7 +157,8 @@ public class BanCommandGroup : CommandGroup
|
||||||
return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
|
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)
|
if (duration is not null)
|
||||||
{
|
{
|
||||||
builder.AppendBulletPoint(
|
builder.AppendBulletPoint(
|
||||||
|
@ -221,10 +224,10 @@ public class BanCommandGroup : CommandGroup
|
||||||
/// <seealso cref="ExecuteBanAsync" />
|
/// <seealso cref="ExecuteBanAsync" />
|
||||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||||
[Command("unban")]
|
[Command("unban")]
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
[DiscordDefaultDMPermission(false)]
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||||
[Description("Unban user")]
|
[Description("Unban user")]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
|
@ -286,7 +289,8 @@ public class BanCommandGroup : CommandGroup
|
||||||
.WithColour(ColorsList.Green).Build();
|
.WithColour(ColorsList.Green).Build();
|
||||||
|
|
||||||
var title = string.Format(Messages.UserUnbanned, target.GetTag());
|
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(
|
_utility.LogAction(
|
||||||
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);
|
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace Octobot.Commands;
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class KickCommandGroup : CommandGroup
|
public class KickCommandGroup : CommandGroup
|
||||||
{
|
{
|
||||||
|
private readonly AccessControlService _access;
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
private readonly IFeedbackService _feedback;
|
private readonly IFeedbackService _feedback;
|
||||||
|
@ -32,16 +33,16 @@ public class KickCommandGroup : CommandGroup
|
||||||
private readonly IDiscordRestUserAPI _userApi;
|
private readonly IDiscordRestUserAPI _userApi;
|
||||||
private readonly Utility _utility;
|
private readonly Utility _utility;
|
||||||
|
|
||||||
public KickCommandGroup(
|
public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
|
||||||
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
|
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
|
||||||
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
|
IDiscordRestUserAPI userApi, Utility utility)
|
||||||
Utility utility)
|
|
||||||
{
|
{
|
||||||
_context = context;
|
_access = access;
|
||||||
_channelApi = channelApi;
|
_channelApi = channelApi;
|
||||||
_guildData = guildData;
|
_context = context;
|
||||||
_feedback = feedback;
|
_feedback = feedback;
|
||||||
_guildApi = guildApi;
|
_guildApi = guildApi;
|
||||||
|
_guildData = guildData;
|
||||||
_userApi = userApi;
|
_userApi = userApi;
|
||||||
_utility = utility;
|
_utility = utility;
|
||||||
}
|
}
|
||||||
|
@ -59,10 +60,10 @@ public class KickCommandGroup : CommandGroup
|
||||||
/// was kicked and vice-versa.
|
/// was kicked and vice-versa.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[Command("kick", "кик")]
|
[Command("kick", "кик")]
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
[DiscordDefaultDMPermission(false)]
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.KickMembers)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
||||||
[Description("Kick member")]
|
[Description("Kick member")]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
|
@ -115,7 +116,7 @@ public class KickCommandGroup : CommandGroup
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var interactionResult
|
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)
|
if (!interactionResult.IsSuccess)
|
||||||
{
|
{
|
||||||
return ResultExtensions.FromError(interactionResult);
|
return ResultExtensions.FromError(interactionResult);
|
||||||
|
@ -134,7 +135,8 @@ public class KickCommandGroup : CommandGroup
|
||||||
{
|
{
|
||||||
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
|
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
|
||||||
.WithTitle(Messages.YouWereKicked)
|
.WithTitle(Messages.YouWereKicked)
|
||||||
.WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
|
.WithDescription(
|
||||||
|
MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
|
||||||
.WithActionFooter(executor)
|
.WithActionFooter(executor)
|
||||||
.WithCurrentTimestamp()
|
.WithCurrentTimestamp()
|
||||||
.WithColour(ColorsList.Red)
|
.WithColour(ColorsList.Red)
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace Octobot.Commands;
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class MuteCommandGroup : CommandGroup
|
public class MuteCommandGroup : CommandGroup
|
||||||
{
|
{
|
||||||
|
private readonly AccessControlService _access;
|
||||||
private readonly ICommandContext _context;
|
private readonly ICommandContext _context;
|
||||||
private readonly IFeedbackService _feedback;
|
private readonly IFeedbackService _feedback;
|
||||||
private readonly IDiscordRestGuildAPI _guildApi;
|
private readonly IDiscordRestGuildAPI _guildApi;
|
||||||
|
@ -35,14 +36,14 @@ public class MuteCommandGroup : CommandGroup
|
||||||
private readonly IDiscordRestUserAPI _userApi;
|
private readonly IDiscordRestUserAPI _userApi;
|
||||||
private readonly Utility _utility;
|
private readonly Utility _utility;
|
||||||
|
|
||||||
public MuteCommandGroup(
|
public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback,
|
||||||
ICommandContext context, GuildDataService guildData, IFeedbackService feedback,
|
IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility)
|
||||||
IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility)
|
|
||||||
{
|
{
|
||||||
|
_access = access;
|
||||||
_context = context;
|
_context = context;
|
||||||
_guildData = guildData;
|
|
||||||
_feedback = feedback;
|
_feedback = feedback;
|
||||||
_guildApi = guildApi;
|
_guildApi = guildApi;
|
||||||
|
_guildData = guildData;
|
||||||
_userApi = userApi;
|
_userApi = userApi;
|
||||||
_utility = utility;
|
_utility = utility;
|
||||||
}
|
}
|
||||||
|
@ -62,10 +63,10 @@ public class MuteCommandGroup : CommandGroup
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <seealso cref="ExecuteUnmute" />
|
/// <seealso cref="ExecuteUnmute" />
|
||||||
[Command("mute", "мут")]
|
[Command("mute", "мут")]
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
[DiscordDefaultDMPermission(false)]
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||||
[Description("Mute member")]
|
[Description("Mute member")]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
|
@ -127,7 +128,7 @@ public class MuteCommandGroup : CommandGroup
|
||||||
Snowflake channelId, IUser bot, CancellationToken ct = default)
|
Snowflake channelId, IUser bot, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var interactionResult
|
var interactionResult
|
||||||
= await _utility.CheckInteractionsAsync(
|
= await _access.CheckInteractionsAsync(
|
||||||
guildId, executor.ID, target.ID, "Mute", ct);
|
guildId, executor.ID, target.ID, "Mute", ct);
|
||||||
if (!interactionResult.IsSuccess)
|
if (!interactionResult.IsSuccess)
|
||||||
{
|
{
|
||||||
|
@ -239,10 +240,10 @@ public class MuteCommandGroup : CommandGroup
|
||||||
/// <seealso cref="ExecuteMute" />
|
/// <seealso cref="ExecuteMute" />
|
||||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||||
[Command("unmute", "размут")]
|
[Command("unmute", "размут")]
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||||
[DiscordDefaultDMPermission(false)]
|
[DiscordDefaultDMPermission(false)]
|
||||||
[RequireContext(ChannelContext.Guild)]
|
[RequireContext(ChannelContext.Guild)]
|
||||||
[RequireDiscordPermission(DiscordPermission.ModerateMembers)]
|
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||||
[Description("Unmute member")]
|
[Description("Unmute member")]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
|
@ -290,7 +291,7 @@ public class MuteCommandGroup : CommandGroup
|
||||||
IUser bot, CancellationToken ct = default)
|
IUser bot, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var interactionResult
|
var interactionResult
|
||||||
= await _utility.CheckInteractionsAsync(
|
= await _access.CheckInteractionsAsync(
|
||||||
guildId, executor.ID, target.ID, "Unmute", ct);
|
guildId, executor.ID, target.ID, "Unmute", ct);
|
||||||
if (!interactionResult.IsSuccess)
|
if (!interactionResult.IsSuccess)
|
||||||
{
|
{
|
||||||
|
|
|
@ -51,6 +51,7 @@ public class SettingsCommandGroup : CommandGroup
|
||||||
GuildSettings.EventNotificationChannel,
|
GuildSettings.EventNotificationChannel,
|
||||||
GuildSettings.DefaultRole,
|
GuildSettings.DefaultRole,
|
||||||
GuildSettings.MuteRole,
|
GuildSettings.MuteRole,
|
||||||
|
GuildSettings.ModeratorRole,
|
||||||
GuildSettings.EventNotificationRole,
|
GuildSettings.EventNotificationRole,
|
||||||
GuildSettings.EventEarlyNotificationOffset
|
GuildSettings.EventEarlyNotificationOffset
|
||||||
];
|
];
|
||||||
|
|
|
@ -76,6 +76,7 @@ public static class GuildSettings
|
||||||
public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
|
public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
|
||||||
public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
|
public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
|
||||||
public static readonly SnowflakeOption MuteRole = new("MuteRole");
|
public static readonly SnowflakeOption MuteRole = new("MuteRole");
|
||||||
|
public static readonly SnowflakeOption ModeratorRole = new("ModeratorRole");
|
||||||
public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole");
|
public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -26,6 +26,7 @@ public enum AllOptionsEnum
|
||||||
[UsedImplicitly] EventNotificationChannel,
|
[UsedImplicitly] EventNotificationChannel,
|
||||||
[UsedImplicitly] DefaultRole,
|
[UsedImplicitly] DefaultRole,
|
||||||
[UsedImplicitly] MuteRole,
|
[UsedImplicitly] MuteRole,
|
||||||
|
[UsedImplicitly] ModeratorRole,
|
||||||
[UsedImplicitly] EventNotificationRole,
|
[UsedImplicitly] EventNotificationRole,
|
||||||
[UsedImplicitly] EventEarlyNotificationOffset
|
[UsedImplicitly] EventEarlyNotificationOffset
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,9 @@ public sealed class Octobot
|
||||||
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
|
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
|
||||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||||
// Services
|
// Services
|
||||||
.AddSingleton<Utility>()
|
.AddSingleton<AccessControlService>()
|
||||||
.AddSingleton<GuildDataService>()
|
.AddSingleton<GuildDataService>()
|
||||||
|
.AddSingleton<Utility>()
|
||||||
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
|
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
|
||||||
.AddHostedService<MemberUpdateService>()
|
.AddHostedService<MemberUpdateService>()
|
||||||
.AddHostedService<ScheduledEventUpdateService>()
|
.AddHostedService<ScheduledEventUpdateService>()
|
||||||
|
|
176
src/Services/AccessControlService.cs
Normal file
176
src/Services/AccessControlService.cs
Normal file
|
@ -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<Result<bool>> 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<bool>.FromError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasPermission = result.IsSuccess;
|
||||||
|
return hasPermission || (!moderatorRole.Empty() &&
|
||||||
|
data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether or not a member can interact with another member
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guildId">The ID of the guild in which an operation is being performed.</param>
|
||||||
|
/// <param name="interacterId">The executor of the operation.</param>
|
||||||
|
/// <param name="targetId">The target of the operation.</param>
|
||||||
|
/// <param name="action">The operation.</param>
|
||||||
|
/// <param name="ct">The cancellation token for this operation.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>A result which has succeeded with a null string if the member can interact with the target.</item>
|
||||||
|
/// <item>
|
||||||
|
/// A result which has succeeded with a non-null string containing the error message if the member cannot
|
||||||
|
/// interact with the target.
|
||||||
|
/// </item>
|
||||||
|
/// <item>A result which has failed if an error occurred during the execution of this method.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </returns>
|
||||||
|
public async Task<Result<string?>> CheckInteractionsAsync(
|
||||||
|
Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (interacterId == targetId)
|
||||||
|
{
|
||||||
|
return Result<string?>.FromSuccess($"UserCannot{action}Themselves".Localized());
|
||||||
|
}
|
||||||
|
|
||||||
|
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||||
|
if (!botResult.IsDefined(out var bot))
|
||||||
|
{
|
||||||
|
return Result<string?>.FromError(botResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
|
||||||
|
if (!guildResult.IsDefined(out var guild))
|
||||||
|
{
|
||||||
|
return Result<string?>.FromError(guildResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct);
|
||||||
|
if (!targetMemberResult.IsDefined(out var targetMember))
|
||||||
|
{
|
||||||
|
return Result<string?>.FromSuccess(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
|
||||||
|
if (!currentMemberResult.IsDefined(out var currentMember))
|
||||||
|
{
|
||||||
|
return Result<string?>.FromError(currentMemberResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
|
||||||
|
if (!rolesResult.IsDefined(out var roles))
|
||||||
|
{
|
||||||
|
return Result<string?>.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<string?>.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<string?>.FromError(permissionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPermission
|
||||||
|
? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter)
|
||||||
|
: Result<string?>.FromSuccess($"UserCannot{action}Members".Localized());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result<string?> CheckInteractions(
|
||||||
|
string action, IGuild guild, IReadOnlyList<IRole> 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<string?>.FromSuccess($"UserCannot{action}Bot".Localized());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser.ID == guild.OwnerID)
|
||||||
|
{
|
||||||
|
return Result<string?>.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<string?>.FromSuccess($"BotCannot{action}Target".Localized());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interacterUser.ID == guild.OwnerID)
|
||||||
|
{
|
||||||
|
return Result<string?>.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<string?>.FromSuccess(null)
|
||||||
|
: Result<string?>.FromSuccess($"UserCannot{action}Target".Localized());
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,20 +26,20 @@ public sealed partial class MemberUpdateService : BackgroundService
|
||||||
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
|
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private readonly AccessControlService _access;
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly IDiscordRestGuildAPI _guildApi;
|
private readonly IDiscordRestGuildAPI _guildApi;
|
||||||
private readonly GuildDataService _guildData;
|
private readonly GuildDataService _guildData;
|
||||||
private readonly ILogger<MemberUpdateService> _logger;
|
private readonly ILogger<MemberUpdateService> _logger;
|
||||||
private readonly Utility _utility;
|
|
||||||
|
|
||||||
public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
|
public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi,
|
||||||
GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility)
|
IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger<MemberUpdateService> logger)
|
||||||
{
|
{
|
||||||
|
_access = access;
|
||||||
_channelApi = channelApi;
|
_channelApi = channelApi;
|
||||||
_guildApi = guildApi;
|
_guildApi = guildApi;
|
||||||
_guildData = guildData;
|
_guildData = guildData;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_utility = utility;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||||
|
@ -94,7 +94,7 @@ public sealed partial class MemberUpdateService : BackgroundService
|
||||||
}
|
}
|
||||||
|
|
||||||
var interactionResult
|
var interactionResult
|
||||||
= await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct);
|
= await _access.CheckInteractionsAsync(guildId, null, id, "Update", ct);
|
||||||
if (!interactionResult.IsSuccess)
|
if (!interactionResult.IsSuccess)
|
||||||
{
|
{
|
||||||
return ResultExtensions.FromError(interactionResult);
|
return ResultExtensions.FromError(interactionResult);
|
||||||
|
|
|
@ -21,129 +21,13 @@ public sealed class Utility
|
||||||
private readonly IDiscordRestChannelAPI _channelApi;
|
private readonly IDiscordRestChannelAPI _channelApi;
|
||||||
private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
|
private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
|
||||||
private readonly IDiscordRestGuildAPI _guildApi;
|
private readonly IDiscordRestGuildAPI _guildApi;
|
||||||
private readonly IDiscordRestUserAPI _userApi;
|
|
||||||
|
|
||||||
public Utility(
|
public Utility(
|
||||||
IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi,
|
IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi)
|
||||||
IDiscordRestUserAPI userApi)
|
|
||||||
{
|
{
|
||||||
_channelApi = channelApi;
|
_channelApi = channelApi;
|
||||||
_eventApi = eventApi;
|
_eventApi = eventApi;
|
||||||
_guildApi = guildApi;
|
_guildApi = guildApi;
|
||||||
_userApi = userApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether or not a member can interact with another member
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guildId">The ID of the guild in which an operation is being performed.</param>
|
|
||||||
/// <param name="interacterId">The executor of the operation.</param>
|
|
||||||
/// <param name="targetId">The target of the operation.</param>
|
|
||||||
/// <param name="action">The operation.</param>
|
|
||||||
/// <param name="ct">The cancellation token for this operation.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>A result which has succeeded with a null string if the member can interact with the target.</item>
|
|
||||||
/// <item>
|
|
||||||
/// A result which has succeeded with a non-null string containing the error message if the member cannot
|
|
||||||
/// interact with the target.
|
|
||||||
/// </item>
|
|
||||||
/// <item>A result which has failed if an error occurred during the execution of this method.</item>
|
|
||||||
/// </list>
|
|
||||||
/// </returns>
|
|
||||||
public async Task<Result<string?>> CheckInteractionsAsync(
|
|
||||||
Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (interacterId == targetId)
|
|
||||||
{
|
|
||||||
return Result<string?>.FromSuccess($"UserCannot{action}Themselves".Localized());
|
|
||||||
}
|
|
||||||
|
|
||||||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
|
||||||
if (!botResult.IsDefined(out var bot))
|
|
||||||
{
|
|
||||||
return Result<string?>.FromError(botResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
|
|
||||||
if (!guildResult.IsDefined(out var guild))
|
|
||||||
{
|
|
||||||
return Result<string?>.FromError(guildResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct);
|
|
||||||
if (!targetMemberResult.IsDefined(out var targetMember))
|
|
||||||
{
|
|
||||||
return Result<string?>.FromSuccess(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
|
|
||||||
if (!currentMemberResult.IsDefined(out var currentMember))
|
|
||||||
{
|
|
||||||
return Result<string?>.FromError(currentMemberResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
|
|
||||||
if (!rolesResult.IsDefined(out var roles))
|
|
||||||
{
|
|
||||||
return Result<string?>.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<string?>.FromError(interacterResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Result<string?> CheckInteractions(
|
|
||||||
string action, IGuild guild, IReadOnlyList<IRole> 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<string?>.FromSuccess($"UserCannot{action}Bot".Localized());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetUser.ID == guild.OwnerID)
|
|
||||||
{
|
|
||||||
return Result<string?>.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<string?>.FromSuccess($"BotCannot{action}Target".Localized());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interacterUser.ID == guild.OwnerID)
|
|
||||||
{
|
|
||||||
return Result<string?>.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<string?>.FromSuccess(null)
|
|
||||||
: Result<string?>.FromSuccess($"UserCannot{action}Target".Localized());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
Loading…
Reference in a new issue