mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-01-31 00:19:00 +03:00
Merge branch 'master' into remove-unused-locales
Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com>
This commit is contained in:
commit
84e7696939
44 changed files with 808 additions and 364 deletions
|
@ -42,7 +42,7 @@ csharp_space_between_square_brackets = false
|
|||
csharp_style_expression_bodied_accessors = false:warning
|
||||
csharp_style_expression_bodied_constructors = false:warning
|
||||
csharp_style_expression_bodied_methods = false:warning
|
||||
csharp_style_expression_bodied_properties = false:warning
|
||||
csharp_style_expression_bodied_properties = true:warning
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
csharp_style_prefer_utf8_string_literals = true:warning
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<Description>A general-purpose Discord bot for moderation written in C#</Description>
|
||||
<ApplicationIcon>docs/octobot.ico</ApplicationIcon>
|
||||
<GitVersion>false</GitVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiffPlex" Version="1.7.2" />
|
||||
<PackageReference Include="GitInfo" Version="3.3.4" />
|
||||
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
|
|
|
@ -117,13 +117,13 @@
|
|||
<data name="DefaultWelcomeMessage" xml:space="preserve">
|
||||
<value>{0}, welcome to {1}</value>
|
||||
</data>
|
||||
<data name="Sound1" xml:space="preserve">
|
||||
<data name="Generic1" xml:space="preserve">
|
||||
<value>Veemo!</value>
|
||||
</data>
|
||||
<data name="Sound2" xml:space="preserve">
|
||||
<data name="Generic2" xml:space="preserve">
|
||||
<value>Woomy!</value>
|
||||
</data>
|
||||
<data name="Sound3" xml:space="preserve">
|
||||
<data name="Generic3" xml:space="preserve">
|
||||
<value>Ngyes!</value>
|
||||
</data>
|
||||
<data name="YouWereBanned" xml:space="preserve">
|
||||
|
@ -204,6 +204,24 @@
|
|||
<data name="MissingUser" xml:space="preserve">
|
||||
<value>You need to specify a user!</value>
|
||||
</data>
|
||||
<data name="UserCannotBanMembers" xml:space="preserve">
|
||||
<value>You cannot ban users from this guild!</value>
|
||||
</data>
|
||||
<data name="UserCannotManageMessages" xml:space="preserve">
|
||||
<value>You cannot manage messages in this guild!</value>
|
||||
</data>
|
||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>You cannot kick members from this guild!</value>
|
||||
</data>
|
||||
<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>
|
||||
</data>
|
||||
<data name="BotCannotBanMembers" xml:space="preserve">
|
||||
<value>I cannot ban users from this guild!</value>
|
||||
</data>
|
||||
|
@ -546,6 +564,12 @@
|
|||
<data name="ButtonReportIssue" xml:space="preserve">
|
||||
<value>Report an issue</value>
|
||||
</data>
|
||||
<data name="DefaultLeaveMessage" xml:space="preserve">
|
||||
<value>See you soon, {0}!</value>
|
||||
</data>
|
||||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>Leave message</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>Time specified incorrectly!</value>
|
||||
</data>
|
||||
|
@ -618,4 +642,19 @@
|
|||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>Example of a valid input: `1h30m`</value>
|
||||
</data>
|
||||
<data name="Version" xml:space="preserve">
|
||||
<value>Version: {0}</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>Welcome messages channel</value>
|
||||
</data>
|
||||
<data name="ButtonDirty" xml:space="preserve">
|
||||
<value>Can't report an issue in the development version</value>
|
||||
</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>
|
||||
</root>
|
||||
|
|
|
@ -117,13 +117,13 @@
|
|||
<data name="DefaultWelcomeMessage" xml:space="preserve">
|
||||
<value>{0}, добро пожаловать на сервер {1}</value>
|
||||
</data>
|
||||
<data name="Sound1" xml:space="preserve">
|
||||
<data name="Generic1" xml:space="preserve">
|
||||
<value>Виимо!</value>
|
||||
</data>
|
||||
<data name="Sound2" xml:space="preserve">
|
||||
<data name="Generic2" xml:space="preserve">
|
||||
<value>Вууми!</value>
|
||||
</data>
|
||||
<data name="Sound3" xml:space="preserve">
|
||||
<data name="Generic3" xml:space="preserve">
|
||||
<value>Нгьес!</value>
|
||||
</data>
|
||||
<data name="PunishmentExpired" xml:space="preserve">
|
||||
|
@ -201,6 +201,24 @@
|
|||
<data name="MissingUser" xml:space="preserve">
|
||||
<value>Надо указать пользователя!</value>
|
||||
</data>
|
||||
<data name="UserCannotBanMembers" xml:space="preserve">
|
||||
<value>Ты не можешь банить пользователей на этом сервере!</value>
|
||||
</data>
|
||||
<data name="UserCannotManageMessages" xml:space="preserve">
|
||||
<value>Ты не можешь управлять сообщениями этого сервера!</value>
|
||||
</data>
|
||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>Ты не можешь выгонять участников с этого сервера!</value>
|
||||
</data>
|
||||
<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>
|
||||
</data>
|
||||
<data name="BotCannotBanMembers" xml:space="preserve">
|
||||
<value>Я не могу банить пользователей на этом сервере!</value>
|
||||
</data>
|
||||
|
@ -546,6 +564,12 @@
|
|||
<data name="ButtonReportIssue" xml:space="preserve">
|
||||
<value>Сообщить о проблеме</value>
|
||||
</data>
|
||||
<data name="DefaultLeaveMessage" xml:space="preserve">
|
||||
<value>До скорой встречи, {0}!</value>
|
||||
</data>
|
||||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>Сообщение о выходе</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>Неправильно указано время!</value>
|
||||
</data>
|
||||
|
@ -618,4 +642,19 @@
|
|||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>Пример правильного ввода: `1ч30м`</value>
|
||||
</data>
|
||||
<data name="Version" xml:space="preserve">
|
||||
<value>Версия: {0}</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>Канал для приветствий</value>
|
||||
</data>
|
||||
<data name="ButtonDirty" xml:space="preserve">
|
||||
<value>Нельзя сообщить о проблеме в версии под разработкой</value>
|
||||
</data>
|
||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||
<value>Открыть Octobot's Wiki</value>
|
||||
</data>
|
||||
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||
<value>Роль модератора</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -117,13 +117,13 @@
|
|||
<data name="DefaultWelcomeMessage" xml:space="preserve">
|
||||
<value>{0}, добро пожаловать на сервер {1}</value>
|
||||
</data>
|
||||
<data name="Sound1" xml:space="preserve">
|
||||
<data name="Generic1" xml:space="preserve">
|
||||
<value>вииимо!</value>
|
||||
</data>
|
||||
<data name="Sound2" xml:space="preserve">
|
||||
<data name="Generic2" xml:space="preserve">
|
||||
<value>вуууми!</value>
|
||||
</data>
|
||||
<data name="Sound3" xml:space="preserve">
|
||||
<data name="Generic3" xml:space="preserve">
|
||||
<value>нгьес!</value>
|
||||
</data>
|
||||
<data name="YouWereBanned" xml:space="preserve">
|
||||
|
@ -204,6 +204,24 @@
|
|||
<data name="MissingUser" xml:space="preserve">
|
||||
<value>укажи самого шизика</value>
|
||||
</data>
|
||||
<data name="UserCannotBanMembers" xml:space="preserve">
|
||||
<value>бан</value>
|
||||
</data>
|
||||
<data name="UserCannotManageMessages" xml:space="preserve">
|
||||
<value>тебе нельзя иметь власть над сообщениями шизоидов</value>
|
||||
</data>
|
||||
<data name="UserCannotKickMembers" xml:space="preserve">
|
||||
<value>кик шизиков нельзя</value>
|
||||
</data>
|
||||
<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>
|
||||
</data>
|
||||
<data name="BotCannotBanMembers" xml:space="preserve">
|
||||
<value>я не могу ваще никого банить чел.</value>
|
||||
</data>
|
||||
|
@ -546,6 +564,12 @@
|
|||
<data name="ButtonReportIssue" xml:space="preserve">
|
||||
<value>зарепортить баг</value>
|
||||
</data>
|
||||
<data name="DefaultLeaveMessage" xml:space="preserve">
|
||||
<value>ну, мы потеряли {0}</value>
|
||||
</data>
|
||||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>до свидания (типо настройка)</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>ты там правильно напиши таймспан</value>
|
||||
</data>
|
||||
|
@ -618,4 +642,19 @@
|
|||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>правильно пишут так: `1h30m`</value>
|
||||
</data>
|
||||
<data name="Version" xml:space="preserve">
|
||||
<value>{0}</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>канал куда говорить здравствуйте</value>
|
||||
</data>
|
||||
<data name="ButtonDirty" xml:space="preserve">
|
||||
<value>вот иди сам и почини что сломал</value>
|
||||
</data>
|
||||
<data name="ButtonOpenWiki" xml:space="preserve">
|
||||
<value>вики Octobot (жмак)</value>
|
||||
</data>
|
||||
<data name="SettingsModeratorRole" xml:space="preserve">
|
||||
<value>звание админа</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
8
src/Attributes/StaticCallersOnlyAttribute.cs
Normal file
8
src/Attributes/StaticCallersOnlyAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Octobot.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Any property marked with <see cref="StaticCallersOnlyAttribute"/> should only be accessed by static methods.
|
||||
/// Such properties may be used to provide dependencies where it is not possible to acquire them through normal means.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class StaticCallersOnlyAttribute : Attribute;
|
18
src/BuildInfo.cs
Normal file
18
src/BuildInfo.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Octobot;
|
||||
|
||||
public static class BuildInfo
|
||||
{
|
||||
public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
|
||||
|
||||
public const string IssuesUrl = $"{RepositoryUrl}/issues";
|
||||
|
||||
public const string WikiUrl = $"{RepositoryUrl}/wiki";
|
||||
|
||||
private const string Commit = ThisAssembly.Git.Commit;
|
||||
|
||||
private const string Branch = ThisAssembly.Git.Branch;
|
||||
|
||||
public static bool IsDirty => ThisAssembly.Git.IsDirty;
|
||||
|
||||
public static string Version => IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}";
|
||||
}
|
|
@ -73,7 +73,7 @@ public class AboutCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
|
@ -101,26 +101,37 @@ public class AboutCommandGroup : CommandGroup
|
|||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png")
|
||||
.WithFooter(string.Format(Messages.Version, BuildInfo.Version))
|
||||
.Build();
|
||||
|
||||
var repositoryButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonOpenRepository,
|
||||
new PartialEmoji(Name: "🌐"),
|
||||
URL: Octobot.RepositoryUrl
|
||||
URL: BuildInfo.RepositoryUrl
|
||||
);
|
||||
|
||||
var wikiButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonOpenWiki,
|
||||
new PartialEmoji(Name: "📖"),
|
||||
URL: BuildInfo.WikiUrl
|
||||
);
|
||||
|
||||
var issuesButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonReportIssue,
|
||||
BuildInfo.IsDirty
|
||||
? Messages.ButtonDirty
|
||||
: Messages.ButtonReportIssue,
|
||||
new PartialEmoji(Name: "⚠️"),
|
||||
URL: Octobot.IssuesUrl
|
||||
URL: BuildInfo.IssuesUrl,
|
||||
IsDisabled: BuildInfo.IsDirty
|
||||
);
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed,
|
||||
new FeedbackMessageOptions(MessageComponents: new[]
|
||||
{
|
||||
new ActionRowComponent(new[] { repositoryButton, issuesButton })
|
||||
new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton })
|
||||
}), ct);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
@ -88,19 +89,19 @@ public class BanCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return Result.FromError(guildResult);
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guild.ID, CancellationToken);
|
||||
|
@ -128,7 +129,8 @@ public class BanCommandGroup : CommandGroup
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
|
||||
|
@ -141,10 +143,10 @@ 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 Result.FromError(interactionResult);
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
|
@ -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(
|
||||
|
@ -181,17 +184,19 @@ public class BanCommandGroup : CommandGroup
|
|||
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.BannedUntil
|
||||
= duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
|
||||
|
||||
var banResult = await _guildApi.CreateGuildBanAsync(
|
||||
guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
ct: ct);
|
||||
if (!banResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(banResult.Error);
|
||||
memberData.BannedUntil = null;
|
||||
return ResultExtensions.FromError(banResult);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.BannedUntil
|
||||
= duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
|
||||
memberData.Roles.Clear();
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
|
@ -219,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]
|
||||
|
@ -240,14 +245,14 @@ public class BanCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
// Needed to get the tag and avatar
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -274,7 +279,7 @@ public class BanCommandGroup : CommandGroup
|
|||
ct);
|
||||
if (!unbanResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(unbanResult.Error);
|
||||
return ResultExtensions.FromError(unbanResult);
|
||||
}
|
||||
|
||||
data.GetOrCreateMemberData(target.ID).BannedUntil = null;
|
||||
|
@ -284,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);
|
||||
|
|
|
@ -75,20 +75,20 @@ public class ClearCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var messagesResult = await _channelApi.GetChannelMessagesAsync(
|
||||
channelId, limit: amount + 1, ct: CancellationToken);
|
||||
if (!messagesResult.IsDefined(out var messages))
|
||||
{
|
||||
return Result.FromError(messagesResult);
|
||||
return ResultExtensions.FromError(messagesResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -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,21 +138,32 @@ 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);
|
||||
if (!deleteResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(deleteResult.Error);
|
||||
return ResultExtensions.FromError(deleteResult);
|
||||
}
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct);
|
||||
foreach (var log in logEntries)
|
||||
{
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, author is not null
|
||||
? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag())
|
||||
: string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()),
|
||||
log.Builder.ToString(), bot, ColorsList.Red, false, ct);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private sealed class ClearedMessageEntry
|
||||
{
|
||||
public StringBuilder Builder { get; } = new();
|
||||
public int DeletedCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ namespace Octobot.Commands.Events;
|
|||
[UsedImplicitly]
|
||||
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
|
||||
{
|
||||
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public ErrorLoggingPostExecutionEvent(ILogger<ErrorLoggingPostExecutionEvent> logger, IFeedbackService feedback,
|
||||
|
@ -53,13 +53,13 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
|
|||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot)
|
||||
|
@ -70,15 +70,19 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
|
|||
|
||||
var issuesButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonReportIssue,
|
||||
BuildInfo.IsDirty
|
||||
? Messages.ButtonDirty
|
||||
: Messages.ButtonReportIssue,
|
||||
new PartialEmoji(Name: "⚠️"),
|
||||
URL: Octobot.IssuesUrl
|
||||
URL: BuildInfo.IssuesUrl,
|
||||
IsDisabled: BuildInfo.IsDirty
|
||||
);
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed,
|
||||
return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed,
|
||||
new FeedbackMessageOptions(MessageComponents: new[]
|
||||
{
|
||||
new ActionRowComponent(new[] { issuesButton })
|
||||
}), ct);
|
||||
}), ct)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,6 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent
|
|||
{
|
||||
_logger.LogResult(preparationResult, "Error in slash command preparation.");
|
||||
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
@ -80,19 +81,19 @@ public class KickCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return Result.FromError(guildResult);
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -115,10 +116,10 @@ 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 Result.FromError(interactionResult);
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
|
@ -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)
|
||||
|
@ -143,17 +145,19 @@ public class KickCommandGroup : CommandGroup
|
|||
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.Kicked = true;
|
||||
|
||||
var kickResult = await _guildApi.RemoveGuildMemberAsync(
|
||||
guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
ct);
|
||||
if (!kickResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(kickResult.Error);
|
||||
memberData.Kicked = false;
|
||||
return ResultExtensions.FromError(kickResult);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.Roles.Clear();
|
||||
memberData.Kicked = true;
|
||||
|
||||
var title = string.Format(Messages.UserKicked, target.GetTag());
|
||||
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
|
|
|
@ -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]
|
||||
|
@ -85,13 +86,13 @@ public class MuteCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -118,7 +119,8 @@ public class MuteCommandGroup : CommandGroup
|
|||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken);
|
||||
return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot,
|
||||
CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> MuteUserAsync(
|
||||
|
@ -126,11 +128,11 @@ 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)
|
||||
{
|
||||
return Result.FromError(interactionResult);
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
|
@ -143,14 +145,16 @@ public class MuteCommandGroup : CommandGroup
|
|||
|
||||
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
|
||||
|
||||
var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct);
|
||||
var muteMethodResult =
|
||||
await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct);
|
||||
if (!muteMethodResult.IsSuccess)
|
||||
{
|
||||
return muteMethodResult;
|
||||
return ResultExtensions.FromError(muteMethodResult);
|
||||
}
|
||||
|
||||
var title = string.Format(Messages.UserMuted, target.GetTag());
|
||||
var description = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
|
||||
var description = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
|
||||
.AppendBulletPoint(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
|
||||
|
||||
|
@ -236,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]
|
||||
|
@ -257,14 +261,14 @@ public class MuteCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
// Needed to get the tag and avatar
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -287,11 +291,11 @@ 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)
|
||||
{
|
||||
return Result.FromError(interactionResult);
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
|
@ -324,14 +328,14 @@ public class MuteCommandGroup : CommandGroup
|
|||
await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken);
|
||||
if (!removeMuteRoleAsync.IsSuccess)
|
||||
{
|
||||
return Result.FromError(removeMuteRoleAsync.Error);
|
||||
return ResultExtensions.FromError(removeMuteRoleAsync);
|
||||
}
|
||||
|
||||
var removeTimeoutResult =
|
||||
await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken);
|
||||
if (!removeTimeoutResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(removeTimeoutResult.Error);
|
||||
return ResultExtensions.FromError(removeTimeoutResult);
|
||||
}
|
||||
|
||||
var title = string.Format(Messages.UserUnmuted, target.GetTag());
|
||||
|
@ -348,11 +352,12 @@ public class MuteCommandGroup : CommandGroup
|
|||
}
|
||||
|
||||
private async Task<Result> RemoveMuteRoleAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, CancellationToken ct = default)
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (memberData.MutedUntil is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
|
@ -372,7 +377,7 @@ public class MuteCommandGroup : CommandGroup
|
|||
{
|
||||
if (communicationDisabledUntil is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
|
|
|
@ -64,7 +64,7 @@ public class PingCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
|
@ -84,14 +84,14 @@ public class PingCommandGroup : CommandGroup
|
|||
channelId, limit: 1, ct: ct);
|
||||
if (!lastMessageResult.IsDefined(out var lastMessage))
|
||||
{
|
||||
return Result.FromError(lastMessageResult);
|
||||
return ResultExtensions.FromError(lastMessageResult);
|
||||
}
|
||||
|
||||
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
|
||||
.WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized())
|
||||
.WithTitle($"Generic{Random.Shared.Next(1, 4)}".Localized())
|
||||
.WithDescription($"{latency:F0}{Messages.Milliseconds}")
|
||||
.WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red)
|
||||
.WithCurrentTimestamp()
|
||||
|
|
|
@ -63,13 +63,13 @@ public class RemindCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -134,13 +134,13 @@ public class RemindCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -226,13 +226,13 @@ public class RemindCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -343,7 +343,7 @@ public class RemindCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
|
|
@ -39,6 +39,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
[
|
||||
GuildSettings.Language,
|
||||
GuildSettings.WelcomeMessage,
|
||||
GuildSettings.LeaveMessage,
|
||||
GuildSettings.ReceiveStartupMessages,
|
||||
GuildSettings.RemoveRolesOnMute,
|
||||
GuildSettings.ReturnRolesOnRejoin,
|
||||
|
@ -46,9 +47,11 @@ public class SettingsCommandGroup : CommandGroup
|
|||
GuildSettings.RenameHoistedUsers,
|
||||
GuildSettings.PublicFeedbackChannel,
|
||||
GuildSettings.PrivateFeedbackChannel,
|
||||
GuildSettings.WelcomeMessagesChannel,
|
||||
GuildSettings.EventNotificationChannel,
|
||||
GuildSettings.DefaultRole,
|
||||
GuildSettings.MuteRole,
|
||||
GuildSettings.ModeratorRole,
|
||||
GuildSettings.EventNotificationRole,
|
||||
GuildSettings.EventEarlyNotificationOffset
|
||||
];
|
||||
|
@ -96,7 +99,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
|
@ -179,13 +182,13 @@ public class SettingsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -239,7 +242,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||
[Description("Reset settings for this server")]
|
||||
[Description("Reset settings for this guild")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteResetSettingsAsync(
|
||||
[Description("Setting to reset")] AllOptionsEnum? setting = null)
|
||||
|
@ -252,7 +255,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
|
@ -272,7 +275,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
var resetResult = option.Reset(cfg);
|
||||
if (!resetResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(resetResult.Error);
|
||||
return ResultExtensions.FromError(resetResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
|
|
|
@ -35,7 +35,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
public ToolsCommandGroup(
|
||||
ICommandContext context, IFeedbackService feedback,
|
||||
GuildDataService guildData, IDiscordRestGuildAPI guildApi,
|
||||
IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi)
|
||||
IDiscordRestUserAPI userApi)
|
||||
{
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
|
@ -81,13 +81,13 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -262,7 +262,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
/// </returns>
|
||||
[Command("guildinfo")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Shows info current guild")]
|
||||
[Description("Shows info about current guild")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteGuildInfoAsync()
|
||||
{
|
||||
|
@ -274,13 +274,13 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return Result.FromError(guildResult);
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -353,7 +353,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -439,13 +439,13 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
@ -514,7 +514,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEightBallAsync(
|
||||
// let the user think he's actually asking the ball a question
|
||||
string question)
|
||||
[Description("Question to ask")] string question)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
|
@ -524,7 +524,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
|
|
|
@ -13,17 +13,29 @@ public static class GuildSettings
|
|||
public static readonly LanguageOption Language = new("Language", "en");
|
||||
|
||||
/// <summary>
|
||||
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
|
||||
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the guild.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>No message will be sent if set to "off", "disable" or "disabled".</item>
|
||||
/// <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset"</item>
|
||||
/// <item><see cref="Messages.DefaultWelcomeMessage" /> will be sent if set to "default" or "reset".</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GuildMemberJoinedResponder" />
|
||||
public static readonly Option<string> WelcomeMessage = new("WelcomeMessage", "default");
|
||||
|
||||
/// <summary>
|
||||
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a member leaves the guild.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>No message will be sent if set to "off", "disable" or "disabled".</item>
|
||||
/// <item><see cref="Messages.DefaultLeaveMessage" /> will be sent if set to "default" or "reset".</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GuildMemberLeftResponder" />
|
||||
public static readonly Option<string> LeaveMessage = new("LeaveMessage", "default");
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether or not the <see cref="Messages.Ready" /> message should be sent
|
||||
/// in <see cref="PrivateFeedbackChannel" /> on startup.
|
||||
|
@ -56,9 +68,15 @@ public static class GuildSettings
|
|||
/// </summary>
|
||||
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel");
|
||||
|
||||
/// <summary>
|
||||
/// Controls what channel should welcome messages be sent to.
|
||||
/// </summary>
|
||||
public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel");
|
||||
|
||||
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>
|
||||
|
|
|
@ -5,10 +5,9 @@ namespace Octobot.Data;
|
|||
/// </summary>
|
||||
public sealed class MemberData
|
||||
{
|
||||
public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List<Reminder>? reminders = null)
|
||||
public MemberData(ulong id, List<Reminder>? reminders = null)
|
||||
{
|
||||
Id = id;
|
||||
BannedUntil = bannedUntil;
|
||||
if (reminders is not null)
|
||||
{
|
||||
Reminders = reminders;
|
||||
|
|
|
@ -14,6 +14,7 @@ public enum AllOptionsEnum
|
|||
{
|
||||
[UsedImplicitly] Language,
|
||||
[UsedImplicitly] WelcomeMessage,
|
||||
[UsedImplicitly] LeaveMessage,
|
||||
[UsedImplicitly] ReceiveStartupMessages,
|
||||
[UsedImplicitly] RemoveRolesOnMute,
|
||||
[UsedImplicitly] ReturnRolesOnRejoin,
|
||||
|
@ -21,9 +22,11 @@ public enum AllOptionsEnum
|
|||
[UsedImplicitly] RenameHoistedUsers,
|
||||
[UsedImplicitly] PublicFeedbackChannel,
|
||||
[UsedImplicitly] PrivateFeedbackChannel,
|
||||
[UsedImplicitly] WelcomeMessagesChannel,
|
||||
[UsedImplicitly] EventNotificationChannel,
|
||||
[UsedImplicitly] DefaultRole,
|
||||
[UsedImplicitly] MuteRole,
|
||||
[UsedImplicitly] ModeratorRole,
|
||||
[UsedImplicitly] EventNotificationRole,
|
||||
[UsedImplicitly] EventEarlyNotificationOffset
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public sealed class BoolOption : Option<bool>
|
|||
}
|
||||
|
||||
settings[Name] = value;
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static bool TryParseBool(string from, out bool value)
|
||||
|
|
|
@ -35,7 +35,13 @@ public class Option<T> : IOption
|
|||
public virtual Result Set(JsonNode settings, string from)
|
||||
{
|
||||
settings[Name] = from;
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public Result Reset(JsonNode settings)
|
||||
{
|
||||
settings[Name] = null;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -48,10 +54,4 @@ public class Option<T> : IOption
|
|||
var property = settings[Name];
|
||||
return property != null ? property.GetValue<T>() : DefaultValue;
|
||||
}
|
||||
|
||||
public Result Reset(JsonNode settings)
|
||||
{
|
||||
settings[Name] = null;
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ public sealed partial class SnowflakeOption : Option<Snowflake>
|
|||
}
|
||||
|
||||
settings[Name] = parsed;
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[GeneratedRegex("[^0-9]")]
|
||||
|
|
|
@ -22,6 +22,6 @@ public sealed class TimeSpanOption : Option<TimeSpan>
|
|||
}
|
||||
|
||||
settings[Name] = span.ToString();
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public static class ChannelApiExtensions
|
|||
{
|
||||
if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed))
|
||||
{
|
||||
return Result.FromError(embedResult.Value);
|
||||
return ResultExtensions.FromError(embedResult.Value);
|
||||
}
|
||||
|
||||
return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed },
|
||||
|
|
|
@ -32,7 +32,7 @@ public static class CollectionExtensions
|
|||
{
|
||||
return list.Count switch
|
||||
{
|
||||
0 => Result.FromSuccess(),
|
||||
0 => Result.Success,
|
||||
1 => list[0],
|
||||
_ => new AggregateError(list.Cast<IResult>().ToArray())
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ public static class FeedbackServiceExtensions
|
|||
{
|
||||
if (!embedResult.IsDefined(out var embed))
|
||||
{
|
||||
return Result.FromError(embedResult);
|
||||
return ResultExtensions.FromError(embedResult);
|
||||
}
|
||||
|
||||
return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct);
|
||||
|
|
|
@ -22,7 +22,7 @@ public static class GuildScheduledEventExtensions
|
|||
}
|
||||
|
||||
return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime)
|
||||
? Result.FromSuccess()
|
||||
? Result.Success
|
||||
: new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
61
src/Extensions/ResultExtensions.cs
Normal file
61
src/Extensions/ResultExtensions.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Octobot.Extensions;
|
||||
|
||||
public static class ResultExtensions
|
||||
{
|
||||
public static Result FromError(Result result)
|
||||
{
|
||||
LogResultStackTrace(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result FromError<T>(Result<T> result)
|
||||
{
|
||||
var casted = (Result)result;
|
||||
LogResultStackTrace(casted);
|
||||
|
||||
return casted;
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private static void LogResultStackTrace(Result result)
|
||||
{
|
||||
if (Octobot.StaticLogger is null || result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Octobot.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}",
|
||||
result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace());
|
||||
|
||||
var inner = result.Inner;
|
||||
while (inner is { IsSuccess: false })
|
||||
{
|
||||
Octobot.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}",
|
||||
inner.Error.GetType().FullName, inner.Error.Message);
|
||||
|
||||
inner = inner.Inner;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConstructStackTrace()
|
||||
{
|
||||
var stackArray = new StackTrace(3, true).ToString().Split(Environment.NewLine).ToList();
|
||||
for (var i = stackArray.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var frame = stackArray[i];
|
||||
var trimmed = frame.TrimStart();
|
||||
if (trimmed.StartsWith("at System.Threading", StringComparison.Ordinal)
|
||||
|| trimmed.StartsWith("at System.Runtime.CompilerServices", StringComparison.Ordinal))
|
||||
{
|
||||
stackArray.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, stackArray);
|
||||
}
|
||||
}
|
60
src/Messages.Designer.cs
generated
60
src/Messages.Designer.cs
generated
|
@ -66,21 +66,21 @@ namespace Octobot {
|
|||
}
|
||||
}
|
||||
|
||||
internal static string Sound1 {
|
||||
internal static string Generic1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Sound1", resourceCulture);
|
||||
return ResourceManager.GetString("Generic1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Sound2 {
|
||||
internal static string Generic2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Sound2", resourceCulture);
|
||||
return ResourceManager.GetString("Generic2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Sound3 {
|
||||
internal static string Generic3 {
|
||||
get {
|
||||
return ResourceManager.GetString("Sound3", resourceCulture);
|
||||
return ResourceManager.GetString("Generic3", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -959,18 +959,26 @@ namespace Octobot {
|
|||
}
|
||||
}
|
||||
|
||||
internal static string InvalidTimeSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
internal static string DefaultLeaveMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SettingsLeaveMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string InvalidTimeSpan {
|
||||
get {
|
||||
return ResourceManager.GetString("InvalidTimeSpan", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string UserInfoKicked
|
||||
{
|
||||
get
|
||||
{
|
||||
internal static string UserInfoKicked {
|
||||
get {
|
||||
return ResourceManager.GetString("UserInfoKicked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
@ -1106,5 +1114,29 @@ namespace Octobot {
|
|||
return ResourceManager.GetString("TimeSpanExample", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Version {
|
||||
get {
|
||||
return ResourceManager.GetString("Version", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SettingsWelcomeMessagesChannel {
|
||||
get {
|
||||
return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string ButtonDirty {
|
||||
get {
|
||||
return ResourceManager.GetString("ButtonDirty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string ButtonOpenWiki {
|
||||
get {
|
||||
return ResourceManager.GetString("ButtonOpenWiki", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Octobot.Attributes;
|
||||
using Octobot.Commands.Events;
|
||||
using Octobot.Services;
|
||||
using Octobot.Services.Update;
|
||||
|
@ -22,16 +23,17 @@ namespace Octobot;
|
|||
|
||||
public sealed class Octobot
|
||||
{
|
||||
public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
|
||||
public const string IssuesUrl = $"{RepositoryUrl}/issues";
|
||||
|
||||
public static readonly AllowedMentions NoMentions = new(
|
||||
Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>());
|
||||
|
||||
[StaticCallersOnly]
|
||||
public static ILogger<Octobot>? StaticLogger { get; private set; }
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
|
||||
var services = host.Services;
|
||||
StaticLogger = services.GetRequiredService<ILogger<Octobot>>();
|
||||
|
||||
var slashService = services.GetRequiredService<SlashService>();
|
||||
// Providing a guild ID to this call will result in command duplicates!
|
||||
|
@ -86,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>()
|
||||
|
|
|
@ -42,7 +42,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
{
|
||||
if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var guild = gatewayEvent.Guild.AsT0;
|
||||
|
@ -57,7 +57,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
if (data.DataLoadFailed)
|
||||
|
@ -68,27 +68,23 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct);
|
||||
if (!ownerResult.IsDefined(out var owner))
|
||||
{
|
||||
return Result.FromError(ownerResult);
|
||||
return ResultExtensions.FromError(ownerResult);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members",
|
||||
guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount);
|
||||
|
||||
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
|
||||
|| !GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var i = Random.Shared.Next(1, 4);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
|
||||
.WithTitle($"Sound{i}".Localized())
|
||||
.WithTitle($"Generic{i}".Localized())
|
||||
.WithDescription(Messages.Ready)
|
||||
.WithCurrentTimestamp()
|
||||
.WithColour(ColorsList.Blue)
|
||||
|
@ -103,7 +99,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct);
|
||||
if (!channelResult.IsDefined(out var channel))
|
||||
{
|
||||
return Result.FromError(channelResult);
|
||||
return ResultExtensions.FromError(channelResult);
|
||||
}
|
||||
|
||||
var errorEmbed = new EmbedBuilder()
|
||||
|
@ -115,9 +111,12 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
|
||||
var issuesButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonReportIssue,
|
||||
BuildInfo.IsDirty
|
||||
? Messages.ButtonDirty
|
||||
: Messages.ButtonReportIssue,
|
||||
new PartialEmoji(Name: "⚠️"),
|
||||
URL: Octobot.IssuesUrl
|
||||
URL: BuildInfo.IssuesUrl,
|
||||
IsDisabled: BuildInfo.IsDirty
|
||||
);
|
||||
|
||||
return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed,
|
||||
|
|
|
@ -48,13 +48,13 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct);
|
||||
if (!returnRolesResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(returnRolesResult.Error);
|
||||
return ResultExtensions.FromError(returnRolesResult);
|
||||
}
|
||||
|
||||
if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||
if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
|
||||
|| GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
@ -65,7 +65,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return Result.FromError(guildResult);
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
|
@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
.Build();
|
||||
|
||||
return await _channelApi.CreateMessageWithEmbedResultAsync(
|
||||
GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed,
|
||||
GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
|
||||
allowedMentions: Octobot.NoMentions, ct: ct);
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
{
|
||||
if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var assignRoles = new List<Snowflake>();
|
||||
|
|
72
src/Responders/GuildMemberLeftResponder.cs
Normal file
72
src/Responders/GuildMemberLeftResponder.cs
Normal file
|
@ -0,0 +1,72 @@
|
|||
using JetBrains.Annotations;
|
||||
using Octobot.Data;
|
||||
using Octobot.Extensions;
|
||||
using Octobot.Services;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Octobot.Responders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending a guild's <see cref="GuildSettings.LeaveMessage" /> if one is set.
|
||||
/// </summary>
|
||||
/// <seealso cref="GuildSettings.LeaveMessage" />
|
||||
[UsedImplicitly]
|
||||
public class GuildMemberLeftResponder : IResponder<IGuildMemberRemove>
|
||||
{
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
|
||||
public GuildMemberLeftResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi)
|
||||
{
|
||||
_channelApi = channelApi;
|
||||
_guildData = guildData;
|
||||
_guildApi = guildApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildMemberRemove gatewayEvent, CancellationToken ct = default)
|
||||
{
|
||||
var user = gatewayEvent.User;
|
||||
var data = await _guildData.GetData(gatewayEvent.GuildID, ct);
|
||||
var cfg = data.Settings;
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(user.ID);
|
||||
if (memberData.BannedUntil is not null || memberData.Kicked)
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
|
||||
|| GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled")
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var leaveMessage = GuildSettings.LeaveMessage.Get(cfg) is "default" or "reset"
|
||||
? Messages.DefaultLeaveMessage
|
||||
: GuildSettings.LeaveMessage.Get(cfg);
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(leaveMessage, user.GetTag(), guild.Name), user)
|
||||
.WithGuildFooter(guild)
|
||||
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||
.WithColour(ColorsList.Black)
|
||||
.Build();
|
||||
|
||||
return await _channelApi.CreateMessageWithEmbedResultAsync(
|
||||
GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
|
||||
allowedMentions: Octobot.NoMentions, ct: ct);
|
||||
}
|
||||
}
|
|
@ -33,6 +33,6 @@ public class GuildUnloadedResponder : IResponder<IGuildDelete>
|
|||
_logger.LogInformation("Unloaded guild {GuildId}", guildId);
|
||||
}
|
||||
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,31 +39,31 @@ public class MessageDeletedResponder : IResponder<IMessageDelete>
|
|||
{
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
|
||||
if (!messageResult.IsDefined(out var message))
|
||||
{
|
||||
return Result.FromError(messageResult);
|
||||
return ResultExtensions.FromError(messageResult);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message.Content))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
|
||||
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
|
||||
if (!auditLogResult.IsDefined(out var auditLogPage))
|
||||
{
|
||||
return Result.FromError(auditLogResult);
|
||||
return ResultExtensions.FromError(auditLogResult);
|
||||
}
|
||||
|
||||
var auditLog = auditLogPage.AuditLogEntries.Single();
|
||||
|
@ -78,15 +78,16 @@ public class MessageDeletedResponder : IResponder<IMessageDelete>
|
|||
|
||||
if (!deleterResult.IsDefined(out var deleter))
|
||||
{
|
||||
return Result.FromError(deleterResult);
|
||||
return ResultExtensions.FromError(deleterResult);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var builder = new StringBuilder().AppendLine(
|
||||
string.Format(Messages.DescriptionActionJumpToChannel,
|
||||
Mention.Channel(gatewayEvent.ChannelID)))
|
||||
.AppendLine(message.Content.InBlockCode());
|
||||
var builder = new StringBuilder()
|
||||
.AppendLine(message.Content.InBlockCode())
|
||||
.AppendLine(
|
||||
string.Format(Messages.DescriptionActionJumpToChannel, Mention.Channel(gatewayEvent.ChannelID))
|
||||
);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(
|
||||
|
|
|
@ -46,30 +46,18 @@ public class MessageEditedResponder : IResponder<IMessageUpdate>
|
|||
return new ArgumentNullError(nameof(gatewayEvent.ChannelID));
|
||||
}
|
||||
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId))
|
||||
if (!gatewayEvent.GuildID.IsDefined(out var guildId)
|
||||
|| !gatewayEvent.Author.IsDefined(out var author)
|
||||
|| !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)
|
||||
|| !gatewayEvent.Content.IsDefined(out var newContent))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
|
||||
{
|
||||
return Result.FromSuccess(); // The message wasn't actually edited
|
||||
}
|
||||
|
||||
if (!gatewayEvent.Content.IsDefined(out var newContent))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, ct);
|
||||
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
|
||||
|
@ -78,12 +66,12 @@ public class MessageEditedResponder : IResponder<IMessageUpdate>
|
|||
if (!messageResult.IsDefined(out var message))
|
||||
{
|
||||
_ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (message.Content == newContent)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
// Custom event responders are called earlier than responders responsible for message caching
|
||||
|
@ -101,10 +89,11 @@ public class MessageEditedResponder : IResponder<IMessageUpdate>
|
|||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
var builder = new StringBuilder().AppendLine(
|
||||
string.Format(Messages.DescriptionActionJumpToMessage,
|
||||
$"https://discord.com/channels/{guildId}/{channelId}/{messageId}"))
|
||||
.AppendLine(diff.AsMarkdown());
|
||||
var builder = new StringBuilder()
|
||||
.AppendLine(diff.AsMarkdown())
|
||||
.AppendLine(string.Format(Messages.DescriptionActionJumpToMessage,
|
||||
$"https://discord.com/channels/{guildId}/{channelId}/{messageId}")
|
||||
);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
|
||||
|
|
|
@ -34,6 +34,6 @@ public class MessageCreateResponder : IResponder<IMessageCreate>
|
|||
"лан" => "https://i.ibb.co/VYH2QLc/lan.jpg",
|
||||
_ => default(Optional<string>)
|
||||
});
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
}
|
||||
|
|
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,10 +94,10 @@ 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 Result.FromError(interactionResult);
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
var canInteract = interactionResult.Entity is null;
|
||||
|
@ -121,7 +121,7 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
|
||||
if (!canInteract)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct);
|
||||
|
@ -148,7 +148,14 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
{
|
||||
if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct);
|
||||
if (!existingBanResult.IsDefined())
|
||||
{
|
||||
data.BannedUntil = null;
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
||||
|
@ -166,7 +173,7 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
{
|
||||
if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
|
@ -202,7 +209,7 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
|
||||
if (!usernameChanged)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var newNickname = string.Concat(characterList.ToArray());
|
||||
|
@ -223,12 +230,13 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
{
|
||||
if (DateTimeOffset.UtcNow < reminder.At)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
|
||||
.AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
|
||||
.AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage,
|
||||
$"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.Reminder, user.GetTag()), user)
|
||||
|
@ -240,10 +248,10 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct);
|
||||
if (!messageResult.IsSuccess)
|
||||
{
|
||||
return messageResult;
|
||||
return ResultExtensions.FromError(messageResult);
|
||||
}
|
||||
|
||||
data.Reminders.Remove(reminder);
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
|
||||
if (!eventsResult.IsDefined(out var events))
|
||||
{
|
||||
return Result.FromError(eventsResult);
|
||||
return ResultExtensions.FromError(eventsResult);
|
||||
}
|
||||
|
||||
SyncScheduledEvents(data, events);
|
||||
|
@ -147,7 +147,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
|| eventData.EarlyNotificationSent
|
||||
|| DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct);
|
||||
|
@ -182,7 +182,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
{
|
||||
if (GuildSettings.EventNotificationChannel.Get(settings).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
if (!scheduledEvent.Creator.IsDefined(out var creator))
|
||||
|
@ -204,7 +204,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
|
||||
if (!embedDescriptionResult.IsDefined(out var embedDescription))
|
||||
{
|
||||
return Result.FromError(embedDescriptionResult);
|
||||
return ResultExtensions.FromError(embedDescriptionResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
|
@ -283,7 +283,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
|
||||
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var embedDescriptionResult = scheduledEvent.EntityType switch
|
||||
|
@ -298,12 +298,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
scheduledEvent, data, ct);
|
||||
if (!contentResult.IsDefined(out var content))
|
||||
{
|
||||
return Result.FromError(contentResult);
|
||||
return ResultExtensions.FromError(contentResult);
|
||||
}
|
||||
|
||||
if (!embedDescriptionResult.IsDefined(out var embedDescription))
|
||||
{
|
||||
return Result.FromError(embedDescriptionResult);
|
||||
return ResultExtensions.FromError(embedDescriptionResult);
|
||||
}
|
||||
|
||||
var startedEmbed = new EmbedBuilder()
|
||||
|
@ -324,7 +324,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
|
||||
{
|
||||
data.ScheduledEvents.Remove(eventData.Id);
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var completedEmbed = new EmbedBuilder()
|
||||
|
@ -356,7 +356,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
|
||||
{
|
||||
data.ScheduledEvents.Remove(eventData.Id);
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
|
@ -409,14 +409,14 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|
|||
{
|
||||
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var contentResult = await _utility.GetEventNotificationMentions(
|
||||
scheduledEvent, data, ct);
|
||||
if (!contentResult.IsDefined(out var content))
|
||||
{
|
||||
return Result.FromError(contentResult);
|
||||
return ResultExtensions.FromError(contentResult);
|
||||
}
|
||||
|
||||
var earlyResult = new EmbedBuilder()
|
||||
|
|
|
@ -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