1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-01-31 17:19: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:
Macintxsh 2024-03-26 18:47:07 +03:00
commit 187f21f1bf
Signed by: mctaylors
GPG key ID: 7181BEBE676903C1
17 changed files with 287 additions and 177 deletions

View file

@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: ReSharper CLI InspectCode - name: ReSharper CLI InspectCode
uses: muno92/resharper_inspectcode@1.11.7 uses: muno92/resharper_inspectcode@1.11.8
with: with:
solutionPath: ./Octobot.sln solutionPath: ./Octobot.sln
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor

View file

@ -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>
@ -674,6 +677,9 @@
</data> </data>
<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 name="SettingsModeratorRole" xml:space="preserve">
<value>Moderator role</value>
</data> </data>
<data name="UserWarned" xml:space="preserve"> <data name="UserWarned" xml:space="preserve">
<value>{0} received a warning</value> <value>{0} received a warning</value>

View file

@ -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>
@ -674,6 +677,9 @@
</data> </data>
<data name="ButtonOpenWiki" xml:space="preserve"> <data name="ButtonOpenWiki" xml:space="preserve">
<value>Открыть Octobot's Wiki</value> <value>Открыть Octobot's Wiki</value>
</data>
<data name="SettingsModeratorRole" xml:space="preserve">
<value>Роль модератора</value>
</data> </data>
<data name="UserWarned" xml:space="preserve"> <data name="UserWarned" xml:space="preserve">
<value>{0} получил предупреждение</value> <value>{0} получил предупреждение</value>

View file

@ -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>
@ -674,6 +677,9 @@
</data> </data>
<data name="ButtonOpenWiki" xml:space="preserve"> <data name="ButtonOpenWiki" xml:space="preserve">
<value>вики Octobot (жмак)</value> <value>вики Octobot (жмак)</value>
</data>
<data name="SettingsModeratorRole" xml:space="preserve">
<value>звание админа</value>
</data> </data>
<data name="UserWarned" xml:space="preserve"> <data name="UserWarned" xml:space="preserve">
<value>{0} схлопотал варн</value> <value>{0} схлопотал варн</value>

View file

@ -2,15 +2,15 @@
public static class BuildInfo 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; public static bool IsDirty => ThisAssembly.Git.IsDirty;

View file

@ -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
} }
public async Task<Result> BanUserAsync( 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) 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);

View file

@ -102,7 +102,9 @@ public class ClearCommandGroup : CommandGroup
CancellationToken ct = default) CancellationToken ct = default)
{ {
var idList = new List<Snowflake>(messages.Count); 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...') for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...')
{ {
var message = messages[i]; var message = messages[i];
@ -112,8 +114,17 @@ public class ClearCommandGroup : CommandGroup
} }
idList.Add(message.ID); 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) if (idList.Count == 0)
@ -127,7 +138,6 @@ public class ClearCommandGroup : CommandGroup
var title = author is not null var title = author is not null
? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag()) ? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag())
: string.Format(Messages.MessagesCleared, idList.Count.ToString()); : string.Format(Messages.MessagesCleared, idList.Count.ToString());
var description = builder.ToString();
var deleteResult = await _channelApi.BulkDeleteMessagesAsync( var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId, idList, executor.GetTag().EncodeHeader(), ct); channelId, idList, executor.GetTag().EncodeHeader(), ct);
@ -136,12 +146,24 @@ public class ClearCommandGroup : CommandGroup
return ResultExtensions.FromError(deleteResult); return ResultExtensions.FromError(deleteResult);
} }
foreach (var log in logEntries)
{
_utility.LogAction( _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) var embed = new EmbedBuilder().WithSmallTitle(title, bot)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
} }
private sealed class ClearedMessageEntry
{
public StringBuilder Builder { get; } = new();
public int DeletedCount { get; set; }
}
} }

View file

@ -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)

View file

@ -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)
{ {

View file

@ -53,6 +53,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,
GuildSettings.WarnPunishmentDuration GuildSettings.WarnPunishmentDuration

View file

@ -80,6 +80,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>

View file

@ -28,6 +28,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,
[UsedImplicitly] WarnPunishmentDuration [UsedImplicitly] WarnPunishmentDuration

View file

@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Remora.Discord.Commands.Extensions;
using Remora.Results; using Remora.Results;
namespace Octobot.Extensions; namespace Octobot.Extensions;
@ -19,7 +18,7 @@ public static class LoggerExtensions
/// <param name="message">The message to use if this result has failed.</param> /// <param name="message">The message to use if this result has failed.</param>
public static void LogResult(this ILogger logger, IResult result, string? message = "") public static void LogResult(this ILogger logger, IResult result, string? message = "")
{ {
if (result.IsSuccess || result.Error.IsUserOrEnvironmentError()) if (result.IsSuccess)
{ {
return; return;
} }

View file

@ -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>()

View 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());
}
}

View file

@ -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);

View file

@ -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>