mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-01-31 09:09:00 +03:00
Merge remote-tracking branch 'origin/master' into warn
# Conflicts: # locale/Messages.resx # locale/Messages.ru.resx # locale/Messages.tt-ru.resx # src/Commands/BanCommandGroup.cs
This commit is contained in:
commit
187f21f1bf
17 changed files with 287 additions and 177 deletions
2
.github/workflows/build-pr.yml
vendored
2
.github/workflows/build-pr.yml
vendored
|
@ -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
|
||||
|
|
|
@ -231,8 +231,11 @@
|
|||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>You cannot kick members from this guild!</value>
|
||||
</data>
|
||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
||||
<value>You cannot moderate members in this guild!</value>
|
||||
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||
<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 name="UserCannotManageGuild" xml:space="preserve">
|
||||
<value>You cannot manage this guild!</value>
|
||||
|
@ -674,6 +677,9 @@
|
|||
</data>
|
||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||
<value>Open Octobot's Wiki</value>
|
||||
</data>
|
||||
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||
<value>Moderator role</value>
|
||||
</data>
|
||||
<data name="UserWarned" xml:space="preserve">
|
||||
<value>{0} received a warning</value>
|
||||
|
|
|
@ -228,8 +228,11 @@
|
|||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>Ты не можешь выгонять участников с этого сервера!</value>
|
||||
</data>
|
||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
||||
<value>Ты не можешь модерировать участников этого сервера!</value>
|
||||
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||
<value>Ты не можешь глушить участников этого сервера!</value>
|
||||
</data>
|
||||
<data name="UserCannotUnmuteMembers" xml:space="preserve">
|
||||
<value>Ты не можешь разглушать участников этого сервера!</value>
|
||||
</data>
|
||||
<data name="UserCannotManageGuild" xml:space="preserve">
|
||||
<value>Ты не можешь настраивать этот сервер!</value>
|
||||
|
@ -674,6 +677,9 @@
|
|||
</data>
|
||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||
<value>Открыть Octobot's Wiki</value>
|
||||
</data>
|
||||
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||
<value>Роль модератора</value>
|
||||
</data>
|
||||
<data name="UserWarned" xml:space="preserve">
|
||||
<value>{0} получил предупреждение</value>
|
||||
|
|
|
@ -231,8 +231,11 @@
|
|||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>кик шизиков нельзя</value>
|
||||
</data>
|
||||
<data name="UserCannotModerateMembers" xml:space="preserve">
|
||||
<value>тебе нельзя управлять шизоидами</value>
|
||||
<data name="UserCannotMuteMembers" xml:space="preserve">
|
||||
<value>тебе нельзя мутить шизоидов</value>
|
||||
</data>
|
||||
<data name="UserCannotUnmuteMembers" xml:space="preserve">
|
||||
<value>тебе нельзя раззамучивать шизоидов</value>
|
||||
</data>
|
||||
<data name="UserCannotManageGuild" xml:space="preserve">
|
||||
<value>тебе нельзя редактировать дурку</value>
|
||||
|
@ -674,6 +677,9 @@
|
|||
</data>
|
||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||
<value>вики Octobot (жмак)</value>
|
||||
</data>
|
||||
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||
<value>звание админа</value>
|
||||
</data>
|
||||
<data name="UserWarned" xml:space="preserve">
|
||||
<value>{0} схлопотал варн</value>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
|||
/// </returns>
|
||||
/// <seealso cref="ExecuteUnban" />
|
||||
[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
|
|||
}
|
||||
|
||||
public 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)
|
||||
{
|
||||
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
|
|||
/// <seealso cref="ExecuteBanAsync" />
|
||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||
[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);
|
||||
|
|
|
@ -102,7 +102,9 @@ public class ClearCommandGroup : CommandGroup
|
|||
CancellationToken ct = default)
|
||||
{
|
||||
var idList = new List<Snowflake>(messages.Count);
|
||||
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine();
|
||||
|
||||
var logEntries = new List<ClearedMessageEntry> { 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);
|
||||
}
|
||||
|
||||
foreach (var log in logEntries)
|
||||
{
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct);
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
/// </returns>
|
||||
[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)
|
||||
|
|
|
@ -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
|
|||
/// </returns>
|
||||
/// <seealso cref="ExecuteUnmute" />
|
||||
[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
|
|||
/// <seealso cref="ExecuteMute" />
|
||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||
[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)
|
||||
{
|
||||
|
|
|
@ -53,6 +53,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
GuildSettings.EventNotificationChannel,
|
||||
GuildSettings.DefaultRole,
|
||||
GuildSettings.MuteRole,
|
||||
GuildSettings.ModeratorRole,
|
||||
GuildSettings.EventNotificationRole,
|
||||
GuildSettings.EventEarlyNotificationOffset,
|
||||
GuildSettings.WarnPunishmentDuration
|
||||
|
|
|
@ -80,6 +80,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");
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -28,6 +28,7 @@ public enum AllOptionsEnum
|
|||
[UsedImplicitly] EventNotificationChannel,
|
||||
[UsedImplicitly] DefaultRole,
|
||||
[UsedImplicitly] MuteRole,
|
||||
[UsedImplicitly] ModeratorRole,
|
||||
[UsedImplicitly] EventNotificationRole,
|
||||
[UsedImplicitly] EventEarlyNotificationOffset,
|
||||
[UsedImplicitly] WarnPunishmentDuration
|
||||
|
|
|
@ -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
|
|||
/// <param name="message">The message to use if this result has failed.</param>
|
||||
public static void LogResult(this ILogger logger, IResult result, string? message = "")
|
||||
{
|
||||
if (result.IsSuccess || result.Error.IsUserOrEnvironmentError())
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -88,8 +88,9 @@ public sealed class Octobot
|
|||
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
|
||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||
// Services
|
||||
.AddSingleton<Utility>()
|
||||
.AddSingleton<AccessControlService>()
|
||||
.AddSingleton<GuildDataService>()
|
||||
.AddSingleton<Utility>()
|
||||
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
|
||||
.AddHostedService<MemberUpdateService>()
|
||||
.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 botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
|
||||
if (!botMemberResult.IsDefined(out var botMember))
|
||||
{
|
||||
return Result<string?>.FromError(botMemberResult);
|
||||
}
|
||||
|
||||
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, botMember, botMember);
|
||||
}
|
||||
|
||||
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, botMember, interacter)
|
||||
: Result<string?>.FromSuccess($"UserCannot{action}Members".Localized());
|
||||
}
|
||||
|
||||
private static Result<string?> CheckInteractions(
|
||||
string action, IGuild guild, IReadOnlyList<IRole> roles, IGuildMember targetMember, IGuildMember botMember,
|
||||
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 (botMember.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 => botMember.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"
|
||||
];
|
||||
|
||||
private readonly AccessControlService _access;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly ILogger<MemberUpdateService> _logger;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
|
||||
GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility)
|
||||
public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi,
|
||||
IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger<MemberUpdateService> 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
|
Loading…
Reference in a new issue