1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-01-31 09:09: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:
Macintxsh 2024-03-24 21:42:50 +03:00 committed by GitHub
commit 84e7696939
Signed by: GitHub
GPG key ID: B5690EEEBB952194
44 changed files with 808 additions and 364 deletions

View file

@ -42,7 +42,7 @@ csharp_space_between_square_brackets = false
csharp_style_expression_bodied_accessors = false:warning csharp_style_expression_bodied_accessors = false:warning
csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_methods = 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_namespace_declarations = file_scoped:warning
csharp_style_prefer_utf8_string_literals = true:warning csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_var_elsewhere = true:warning csharp_style_var_elsewhere = true:warning

View file

@ -17,10 +17,12 @@
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<Description>A general-purpose Discord bot for moderation written in C#</Description> <Description>A general-purpose Discord bot for moderation written in C#</Description>
<ApplicationIcon>docs/octobot.ico</ApplicationIcon> <ApplicationIcon>docs/octobot.ico</ApplicationIcon>
<GitVersion>false</GitVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.2" /> <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="Humanizer.Core.ru" Version="2.14.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />

View file

@ -117,13 +117,13 @@
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, welcome to {1}</value> <value>{0}, welcome to {1}</value>
</data> </data>
<data name="Sound1" xml:space="preserve"> <data name="Generic1" xml:space="preserve">
<value>Veemo!</value> <value>Veemo!</value>
</data> </data>
<data name="Sound2" xml:space="preserve"> <data name="Generic2" xml:space="preserve">
<value>Woomy!</value> <value>Woomy!</value>
</data> </data>
<data name="Sound3" xml:space="preserve"> <data name="Generic3" xml:space="preserve">
<value>Ngyes!</value> <value>Ngyes!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
@ -204,6 +204,24 @@
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>You need to specify a user!</value> <value>You need to specify a user!</value>
</data> </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"> <data name="BotCannotBanMembers" xml:space="preserve">
<value>I cannot ban users from this guild!</value> <value>I cannot ban users from this guild!</value>
</data> </data>
@ -546,6 +564,12 @@
<data name="ButtonReportIssue" xml:space="preserve"> <data name="ButtonReportIssue" xml:space="preserve">
<value>Report an issue</value> <value>Report an issue</value>
</data> </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"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>Time specified incorrectly!</value> <value>Time specified incorrectly!</value>
</data> </data>
@ -618,4 +642,19 @@
<data name="TimeSpanExample" xml:space="preserve"> <data name="TimeSpanExample" xml:space="preserve">
<value>Example of a valid input: `1h30m`</value> <value>Example of a valid input: `1h30m`</value>
</data> </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> </root>

View file

@ -117,13 +117,13 @@
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value> <value>{0}, добро пожаловать на сервер {1}</value>
</data> </data>
<data name="Sound1" xml:space="preserve"> <data name="Generic1" xml:space="preserve">
<value>Виимо!</value> <value>Виимо!</value>
</data> </data>
<data name="Sound2" xml:space="preserve"> <data name="Generic2" xml:space="preserve">
<value>Вууми!</value> <value>Вууми!</value>
</data> </data>
<data name="Sound3" xml:space="preserve"> <data name="Generic3" xml:space="preserve">
<value>Нгьес!</value> <value>Нгьес!</value>
</data> </data>
<data name="PunishmentExpired" xml:space="preserve"> <data name="PunishmentExpired" xml:space="preserve">
@ -201,6 +201,24 @@
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>Надо указать пользователя!</value> <value>Надо указать пользователя!</value>
</data> </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"> <data name="BotCannotBanMembers" xml:space="preserve">
<value>Я не могу банить пользователей на этом сервере!</value> <value>Я не могу банить пользователей на этом сервере!</value>
</data> </data>
@ -546,6 +564,12 @@
<data name="ButtonReportIssue" xml:space="preserve"> <data name="ButtonReportIssue" xml:space="preserve">
<value>Сообщить о проблеме</value> <value>Сообщить о проблеме</value>
</data> </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"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>Неправильно указано время!</value> <value>Неправильно указано время!</value>
</data> </data>
@ -618,4 +642,19 @@
<data name="TimeSpanExample" xml:space="preserve"> <data name="TimeSpanExample" xml:space="preserve">
<value>Пример правильного ввода: `1ч30м`</value> <value>Пример правильного ввода: `1ч30м`</value>
</data> </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> </root>

View file

@ -117,13 +117,13 @@
<data name="DefaultWelcomeMessage" xml:space="preserve"> <data name="DefaultWelcomeMessage" xml:space="preserve">
<value>{0}, добро пожаловать на сервер {1}</value> <value>{0}, добро пожаловать на сервер {1}</value>
</data> </data>
<data name="Sound1" xml:space="preserve"> <data name="Generic1" xml:space="preserve">
<value>вииимо!</value> <value>вииимо!</value>
</data> </data>
<data name="Sound2" xml:space="preserve"> <data name="Generic2" xml:space="preserve">
<value>вуууми!</value> <value>вуууми!</value>
</data> </data>
<data name="Sound3" xml:space="preserve"> <data name="Generic3" xml:space="preserve">
<value>нгьес!</value> <value>нгьес!</value>
</data> </data>
<data name="YouWereBanned" xml:space="preserve"> <data name="YouWereBanned" xml:space="preserve">
@ -204,6 +204,24 @@
<data name="MissingUser" xml:space="preserve"> <data name="MissingUser" xml:space="preserve">
<value>укажи самого шизика</value> <value>укажи самого шизика</value>
</data> </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"> <data name="BotCannotBanMembers" xml:space="preserve">
<value>я не могу ваще никого банить чел.</value> <value>я не могу ваще никого банить чел.</value>
</data> </data>
@ -546,6 +564,12 @@
<data name="ButtonReportIssue" xml:space="preserve"> <data name="ButtonReportIssue" xml:space="preserve">
<value>зарепортить баг</value> <value>зарепортить баг</value>
</data> </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"> <data name="InvalidTimeSpan" xml:space="preserve">
<value>ты там правильно напиши таймспан</value> <value>ты там правильно напиши таймспан</value>
</data> </data>
@ -618,4 +642,19 @@
<data name="TimeSpanExample" xml:space="preserve"> <data name="TimeSpanExample" xml:space="preserve">
<value>правильно пишут так: `1h30m`</value> <value>правильно пишут так: `1h30m`</value>
</data> </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> </root>

View 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
View 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}";
}

View file

@ -73,7 +73,7 @@ public class AboutCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var cfg = await _guildData.GetSettings(guildId, CancellationToken); var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@ -101,26 +101,37 @@ public class AboutCommandGroup : CommandGroup
.WithDescription(builder.ToString()) .WithDescription(builder.ToString())
.WithColour(ColorsList.Cyan) .WithColour(ColorsList.Cyan)
.WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png")
.WithFooter(string.Format(Messages.Version, BuildInfo.Version))
.Build(); .Build();
var repositoryButton = new ButtonComponent( var repositoryButton = new ButtonComponent(
ButtonComponentStyle.Link, ButtonComponentStyle.Link,
Messages.ButtonOpenRepository, Messages.ButtonOpenRepository,
new PartialEmoji(Name: "🌐"), 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( var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link, ButtonComponentStyle.Link,
Messages.ButtonReportIssue, BuildInfo.IsDirty
? Messages.ButtonDirty
: Messages.ButtonReportIssue,
new PartialEmoji(Name: "⚠️"), new PartialEmoji(Name: "⚠️"),
URL: Octobot.IssuesUrl URL: BuildInfo.IssuesUrl,
IsDisabled: BuildInfo.IsDirty
); );
return await _feedback.SendContextualEmbedResultAsync(embed, return await _feedback.SendContextualEmbedResultAsync(embed,
new FeedbackMessageOptions(MessageComponents: new[] new FeedbackMessageOptions(MessageComponents: new[]
{ {
new ActionRowComponent(new[] { repositoryButton, issuesButton }) new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton })
}), ct); }), ct);
} }
} }

View file

@ -28,6 +28,7 @@ namespace Octobot.Commands;
[UsedImplicitly] [UsedImplicitly]
public class BanCommandGroup : CommandGroup public class BanCommandGroup : CommandGroup
{ {
private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
@ -36,16 +37,16 @@ public class BanCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility; private readonly Utility _utility;
public BanCommandGroup( public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestUserAPI userApi, Utility utility)
Utility utility)
{ {
_context = context; _access = access;
_channelApi = channelApi; _channelApi = channelApi;
_guildData = guildData; _context = context;
_feedback = feedback; _feedback = feedback;
_guildApi = guildApi; _guildApi = guildApi;
_guildData = guildData;
_userApi = userApi; _userApi = userApi;
_utility = utility; _utility = utility;
} }
@ -65,10 +66,10 @@ public class BanCommandGroup : CommandGroup
/// </returns> /// </returns>
/// <seealso cref="ExecuteUnban" /> /// <seealso cref="ExecuteUnban" />
[Command("ban", "бан")] [Command("ban", "бан")]
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("Ban user")] [Description("Ban user")]
[UsedImplicitly] [UsedImplicitly]
@ -88,19 +89,19 @@ public class BanCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild)) if (!guildResult.IsDefined(out var guild))
{ {
return Result.FromError(guildResult); return ResultExtensions.FromError(guildResult);
} }
var data = await _guildData.GetData(guild.ID, CancellationToken); var data = await _guildData.GetData(guild.ID, CancellationToken);
@ -128,7 +129,8 @@ public class BanCommandGroup : CommandGroup
} }
private async Task<Result> BanUserAsync( private async Task<Result> BanUserAsync(
IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data,
Snowflake channelId,
IUser bot, CancellationToken ct = default) IUser bot, CancellationToken ct = default)
{ {
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct); var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
@ -141,10 +143,10 @@ public class BanCommandGroup : CommandGroup
} }
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct); = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
{ {
return Result.FromError(interactionResult); return ResultExtensions.FromError(interactionResult);
} }
if (interactionResult.Entity is not null) if (interactionResult.Entity is not null)
@ -155,7 +157,8 @@ public class BanCommandGroup : CommandGroup
return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
} }
var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); var builder =
new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
if (duration is not null) if (duration is not null)
{ {
builder.AppendBulletPoint( builder.AppendBulletPoint(
@ -181,17 +184,19 @@ public class BanCommandGroup : CommandGroup
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); 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( var banResult = await _guildApi.CreateGuildBanAsync(
guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
ct: ct); ct: ct);
if (!banResult.IsSuccess) 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(); memberData.Roles.Clear();
var embed = new EmbedBuilder().WithSmallTitle( var embed = new EmbedBuilder().WithSmallTitle(
@ -219,10 +224,10 @@ public class BanCommandGroup : CommandGroup
/// <seealso cref="ExecuteBanAsync" /> /// <seealso cref="ExecuteBanAsync" />
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" /> /// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
[Command("unban")] [Command("unban")]
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.BanMembers)] [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("Unban user")] [Description("Unban user")]
[UsedImplicitly] [UsedImplicitly]
@ -240,14 +245,14 @@ public class BanCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
// Needed to get the tag and avatar // Needed to get the tag and avatar
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -274,7 +279,7 @@ public class BanCommandGroup : CommandGroup
ct); ct);
if (!unbanResult.IsSuccess) if (!unbanResult.IsSuccess)
{ {
return Result.FromError(unbanResult.Error); return ResultExtensions.FromError(unbanResult);
} }
data.GetOrCreateMemberData(target.ID).BannedUntil = null; data.GetOrCreateMemberData(target.ID).BannedUntil = null;
@ -284,7 +289,8 @@ public class BanCommandGroup : CommandGroup
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
var title = string.Format(Messages.UserUnbanned, target.GetTag()); var title = string.Format(Messages.UserUnbanned, target.GetTag());
var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var description =
new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason));
_utility.LogAction( _utility.LogAction(
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);

View file

@ -75,20 +75,20 @@ public class ClearCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var messagesResult = await _channelApi.GetChannelMessagesAsync( var messagesResult = await _channelApi.GetChannelMessagesAsync(
channelId, limit: amount + 1, ct: CancellationToken); channelId, limit: amount + 1, ct: CancellationToken);
if (!messagesResult.IsDefined(out var messages)) if (!messagesResult.IsDefined(out var messages))
{ {
return Result.FromError(messagesResult); return ResultExtensions.FromError(messagesResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -102,7 +102,9 @@ public class ClearCommandGroup : CommandGroup
CancellationToken ct = default) CancellationToken ct = default)
{ {
var idList = new List<Snowflake>(messages.Count); var idList = new List<Snowflake>(messages.Count);
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine();
var logEntries = new List<ClearedMessageEntry> { new() };
var currentLogEntry = 0;
for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...') for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...')
{ {
var message = messages[i]; var message = messages[i];
@ -112,8 +114,17 @@ public class ClearCommandGroup : CommandGroup
} }
idList.Add(message.ID); idList.Add(message.ID);
builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author)));
builder.Append(message.Content.InBlockCode()); var entry = logEntries[currentLogEntry];
var str = $"{string.Format(Messages.MessageFrom, Mention.User(message.Author))}\n{message.Content.InBlockCode()}";
if (entry.Builder.Length + str.Length > EmbedConstants.MaxDescriptionLength)
{
logEntries.Add(entry = new ClearedMessageEntry());
currentLogEntry++;
}
entry.Builder.Append(str);
entry.DeletedCount++;
} }
if (idList.Count == 0) if (idList.Count == 0)
@ -127,21 +138,32 @@ public class ClearCommandGroup : CommandGroup
var title = author is not null var title = author is not null
? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag()) ? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag())
: string.Format(Messages.MessagesCleared, idList.Count.ToString()); : string.Format(Messages.MessagesCleared, idList.Count.ToString());
var description = builder.ToString();
var deleteResult = await _channelApi.BulkDeleteMessagesAsync( var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId, idList, executor.GetTag().EncodeHeader(), ct); channelId, idList, executor.GetTag().EncodeHeader(), ct);
if (!deleteResult.IsSuccess) if (!deleteResult.IsSuccess)
{ {
return Result.FromError(deleteResult.Error); return ResultExtensions.FromError(deleteResult);
} }
foreach (var log in logEntries)
{
_utility.LogAction( _utility.LogAction(
data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); data.Settings, channelId, executor, author is not null
? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag())
: string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()),
log.Builder.ToString(), bot, ColorsList.Red, false, ct);
}
var embed = new EmbedBuilder().WithSmallTitle(title, bot) var embed = new EmbedBuilder().WithSmallTitle(title, bot)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
} }
private sealed class ClearedMessageEntry
{
public StringBuilder Builder { get; } = new();
public int DeletedCount { get; set; }
}
} }

View file

@ -20,8 +20,8 @@ namespace Octobot.Commands.Events;
[UsedImplicitly] [UsedImplicitly]
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
{ {
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
public ErrorLoggingPostExecutionEvent(ILogger<ErrorLoggingPostExecutionEvent> logger, IFeedbackService feedback, public ErrorLoggingPostExecutionEvent(ILogger<ErrorLoggingPostExecutionEvent> logger, IFeedbackService feedback,
@ -53,13 +53,13 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
if (result.IsSuccess) if (result.IsSuccess)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var botResult = await _userApi.GetCurrentUserAsync(ct); var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot) var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot)
@ -70,15 +70,19 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
var issuesButton = new ButtonComponent( var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link, ButtonComponentStyle.Link,
Messages.ButtonReportIssue, BuildInfo.IsDirty
? Messages.ButtonDirty
: Messages.ButtonReportIssue,
new PartialEmoji(Name: "⚠️"), 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 FeedbackMessageOptions(MessageComponents: new[]
{ {
new ActionRowComponent(new[] { issuesButton }) new ActionRowComponent(new[] { issuesButton })
}), ct); }), ct)
);
} }
} }

View file

@ -33,6 +33,6 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent
{ {
_logger.LogResult(preparationResult, "Error in slash command preparation."); _logger.LogResult(preparationResult, "Error in slash command preparation.");
return Task.FromResult(Result.FromSuccess()); return Task.FromResult(Result.Success);
} }
} }

View file

@ -24,6 +24,7 @@ namespace Octobot.Commands;
[UsedImplicitly] [UsedImplicitly]
public class KickCommandGroup : CommandGroup public class KickCommandGroup : CommandGroup
{ {
private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
@ -32,16 +33,16 @@ public class KickCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility; private readonly Utility _utility;
public KickCommandGroup( public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestUserAPI userApi, Utility utility)
Utility utility)
{ {
_context = context; _access = access;
_channelApi = channelApi; _channelApi = channelApi;
_guildData = guildData; _context = context;
_feedback = feedback; _feedback = feedback;
_guildApi = guildApi; _guildApi = guildApi;
_guildData = guildData;
_userApi = userApi; _userApi = userApi;
_utility = utility; _utility = utility;
} }
@ -59,10 +60,10 @@ public class KickCommandGroup : CommandGroup
/// was kicked and vice-versa. /// was kicked and vice-versa.
/// </returns> /// </returns>
[Command("kick", "кик")] [Command("kick", "кик")]
[DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.KickMembers)] [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
[Description("Kick member")] [Description("Kick member")]
[UsedImplicitly] [UsedImplicitly]
@ -80,19 +81,19 @@ public class KickCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild)) if (!guildResult.IsDefined(out var guild))
{ {
return Result.FromError(guildResult); return ResultExtensions.FromError(guildResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -115,10 +116,10 @@ public class KickCommandGroup : CommandGroup
CancellationToken ct = default) CancellationToken ct = default)
{ {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct); = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
{ {
return Result.FromError(interactionResult); return ResultExtensions.FromError(interactionResult);
} }
if (interactionResult.Entity is not null) if (interactionResult.Entity is not null)
@ -134,7 +135,8 @@ public class KickCommandGroup : CommandGroup
{ {
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereKicked) .WithTitle(Messages.YouWereKicked)
.WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason))) .WithDescription(
MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
.WithActionFooter(executor) .WithActionFooter(executor)
.WithCurrentTimestamp() .WithCurrentTimestamp()
.WithColour(ColorsList.Red) .WithColour(ColorsList.Red)
@ -143,17 +145,19 @@ public class KickCommandGroup : CommandGroup
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
} }
var memberData = data.GetOrCreateMemberData(target.ID);
memberData.Kicked = true;
var kickResult = await _guildApi.RemoveGuildMemberAsync( var kickResult = await _guildApi.RemoveGuildMemberAsync(
guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(), guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(),
ct); ct);
if (!kickResult.IsSuccess) 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.Roles.Clear();
memberData.Kicked = true;
var title = string.Format(Messages.UserKicked, target.GetTag()); var title = string.Format(Messages.UserKicked, target.GetTag());
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));

View file

@ -28,6 +28,7 @@ namespace Octobot.Commands;
[UsedImplicitly] [UsedImplicitly]
public class MuteCommandGroup : CommandGroup public class MuteCommandGroup : CommandGroup
{ {
private readonly AccessControlService _access;
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly IFeedbackService _feedback; private readonly IFeedbackService _feedback;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
@ -35,14 +36,14 @@ public class MuteCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi; private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility; private readonly Utility _utility;
public MuteCommandGroup( public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback,
ICommandContext context, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility)
IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility)
{ {
_access = access;
_context = context; _context = context;
_guildData = guildData;
_feedback = feedback; _feedback = feedback;
_guildApi = guildApi; _guildApi = guildApi;
_guildData = guildData;
_userApi = userApi; _userApi = userApi;
_utility = utility; _utility = utility;
} }
@ -62,10 +63,10 @@ public class MuteCommandGroup : CommandGroup
/// </returns> /// </returns>
/// <seealso cref="ExecuteUnmute" /> /// <seealso cref="ExecuteUnmute" />
[Command("mute", "мут")] [Command("mute", "мут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ModerateMembers)] [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Mute member")] [Description("Mute member")]
[UsedImplicitly] [UsedImplicitly]
@ -85,13 +86,13 @@ public class MuteCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -118,7 +119,8 @@ public class MuteCommandGroup : CommandGroup
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); 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( private async Task<Result> MuteUserAsync(
@ -126,11 +128,11 @@ public class MuteCommandGroup : CommandGroup
Snowflake channelId, IUser bot, CancellationToken ct = default) Snowflake channelId, IUser bot, CancellationToken ct = default)
{ {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync( = await _access.CheckInteractionsAsync(
guildId, executor.ID, target.ID, "Mute", ct); guildId, executor.ID, target.ID, "Mute", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
{ {
return Result.FromError(interactionResult); return ResultExtensions.FromError(interactionResult);
} }
if (interactionResult.Entity is not null) if (interactionResult.Entity is not null)
@ -143,14 +145,16 @@ public class MuteCommandGroup : CommandGroup
var until = DateTimeOffset.UtcNow.Add(duration); // >:) 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) if (!muteMethodResult.IsSuccess)
{ {
return muteMethodResult; return ResultExtensions.FromError(muteMethodResult);
} }
var title = string.Format(Messages.UserMuted, target.GetTag()); 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( .AppendBulletPoint(string.Format(
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
@ -236,10 +240,10 @@ public class MuteCommandGroup : CommandGroup
/// <seealso cref="ExecuteMute" /> /// <seealso cref="ExecuteMute" />
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" /> /// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
[Command("unmute", "размут")] [Command("unmute", "размут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ModerateMembers)] [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Unmute member")] [Description("Unmute member")]
[UsedImplicitly] [UsedImplicitly]
@ -257,14 +261,14 @@ public class MuteCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
// Needed to get the tag and avatar // Needed to get the tag and avatar
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -287,11 +291,11 @@ public class MuteCommandGroup : CommandGroup
IUser bot, CancellationToken ct = default) IUser bot, CancellationToken ct = default)
{ {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync( = await _access.CheckInteractionsAsync(
guildId, executor.ID, target.ID, "Unmute", ct); guildId, executor.ID, target.ID, "Unmute", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
{ {
return Result.FromError(interactionResult); return ResultExtensions.FromError(interactionResult);
} }
if (interactionResult.Entity is not null) if (interactionResult.Entity is not null)
@ -324,14 +328,14 @@ public class MuteCommandGroup : CommandGroup
await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken); await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken);
if (!removeMuteRoleAsync.IsSuccess) if (!removeMuteRoleAsync.IsSuccess)
{ {
return Result.FromError(removeMuteRoleAsync.Error); return ResultExtensions.FromError(removeMuteRoleAsync);
} }
var removeTimeoutResult = var removeTimeoutResult =
await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken); await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken);
if (!removeTimeoutResult.IsSuccess) if (!removeTimeoutResult.IsSuccess)
{ {
return Result.FromError(removeTimeoutResult.Error); return ResultExtensions.FromError(removeTimeoutResult);
} }
var title = string.Format(Messages.UserUnmuted, target.GetTag()); var title = string.Format(Messages.UserUnmuted, target.GetTag());
@ -348,11 +352,12 @@ public class MuteCommandGroup : CommandGroup
} }
private async Task<Result> RemoveMuteRoleAsync( 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) if (memberData.MutedUntil is null)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var unmuteResult = await _guildApi.ModifyGuildMemberAsync( var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
@ -372,7 +377,7 @@ public class MuteCommandGroup : CommandGroup
{ {
if (communicationDisabledUntil is null) if (communicationDisabledUntil is null)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var unmuteResult = await _guildApi.ModifyGuildMemberAsync( var unmuteResult = await _guildApi.ModifyGuildMemberAsync(

View file

@ -64,7 +64,7 @@ public class PingCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var cfg = await _guildData.GetSettings(guildId, CancellationToken); var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@ -84,14 +84,14 @@ public class PingCommandGroup : CommandGroup
channelId, limit: 1, ct: ct); channelId, limit: 1, ct: ct);
if (!lastMessageResult.IsDefined(out var lastMessage)) if (!lastMessageResult.IsDefined(out var lastMessage))
{ {
return Result.FromError(lastMessageResult); return ResultExtensions.FromError(lastMessageResult);
} }
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
} }
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) 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}") .WithDescription($"{latency:F0}{Messages.Milliseconds}")
.WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red) .WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red)
.WithCurrentTimestamp() .WithCurrentTimestamp()

View file

@ -63,13 +63,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -134,13 +134,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -226,13 +226,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -343,7 +343,7 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);

View file

@ -39,6 +39,7 @@ public class SettingsCommandGroup : CommandGroup
[ [
GuildSettings.Language, GuildSettings.Language,
GuildSettings.WelcomeMessage, GuildSettings.WelcomeMessage,
GuildSettings.LeaveMessage,
GuildSettings.ReceiveStartupMessages, GuildSettings.ReceiveStartupMessages,
GuildSettings.RemoveRolesOnMute, GuildSettings.RemoveRolesOnMute,
GuildSettings.ReturnRolesOnRejoin, GuildSettings.ReturnRolesOnRejoin,
@ -46,9 +47,11 @@ public class SettingsCommandGroup : CommandGroup
GuildSettings.RenameHoistedUsers, GuildSettings.RenameHoistedUsers,
GuildSettings.PublicFeedbackChannel, GuildSettings.PublicFeedbackChannel,
GuildSettings.PrivateFeedbackChannel, GuildSettings.PrivateFeedbackChannel,
GuildSettings.WelcomeMessagesChannel,
GuildSettings.EventNotificationChannel, GuildSettings.EventNotificationChannel,
GuildSettings.DefaultRole, GuildSettings.DefaultRole,
GuildSettings.MuteRole, GuildSettings.MuteRole,
GuildSettings.ModeratorRole,
GuildSettings.EventNotificationRole, GuildSettings.EventNotificationRole,
GuildSettings.EventEarlyNotificationOffset GuildSettings.EventEarlyNotificationOffset
]; ];
@ -96,7 +99,7 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var cfg = await _guildData.GetSettings(guildId, CancellationToken); var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@ -179,13 +182,13 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -239,7 +242,7 @@ public class SettingsCommandGroup : CommandGroup
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ManageGuild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)]
[Description("Reset settings for this server")] [Description("Reset settings for this guild")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> ExecuteResetSettingsAsync( public async Task<Result> ExecuteResetSettingsAsync(
[Description("Setting to reset")] AllOptionsEnum? setting = null) [Description("Setting to reset")] AllOptionsEnum? setting = null)
@ -252,7 +255,7 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var cfg = await _guildData.GetSettings(guildId, CancellationToken); var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@ -272,7 +275,7 @@ public class SettingsCommandGroup : CommandGroup
var resetResult = option.Reset(cfg); var resetResult = option.Reset(cfg);
if (!resetResult.IsSuccess) if (!resetResult.IsSuccess)
{ {
return Result.FromError(resetResult.Error); return ResultExtensions.FromError(resetResult);
} }
var embed = new EmbedBuilder().WithSmallTitle( var embed = new EmbedBuilder().WithSmallTitle(

View file

@ -35,7 +35,7 @@ public class ToolsCommandGroup : CommandGroup
public ToolsCommandGroup( public ToolsCommandGroup(
ICommandContext context, IFeedbackService feedback, ICommandContext context, IFeedbackService feedback,
GuildDataService guildData, IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi,
IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi) IDiscordRestUserAPI userApi)
{ {
_context = context; _context = context;
_guildData = guildData; _guildData = guildData;
@ -81,13 +81,13 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -262,7 +262,7 @@ public class ToolsCommandGroup : CommandGroup
/// </returns> /// </returns>
[Command("guildinfo")] [Command("guildinfo")]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[Description("Shows info current guild")] [Description("Shows info about current guild")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> ExecuteGuildInfoAsync() public async Task<Result> ExecuteGuildInfoAsync()
{ {
@ -274,13 +274,13 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild)) if (!guildResult.IsDefined(out var guild))
{ {
return Result.FromError(guildResult); return ResultExtensions.FromError(guildResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -353,7 +353,7 @@ public class ToolsCommandGroup : CommandGroup
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -439,13 +439,13 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor)) if (!executorResult.IsDefined(out var executor))
{ {
return Result.FromError(executorResult); return ResultExtensions.FromError(executorResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);
@ -514,7 +514,7 @@ public class ToolsCommandGroup : CommandGroup
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> ExecuteEightBallAsync( public async Task<Result> ExecuteEightBallAsync(
// let the user think he's actually asking the ball a question // 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 _)) if (!_context.TryGetContextIDs(out var guildId, out _, out _))
{ {
@ -524,7 +524,7 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
var data = await _guildData.GetData(guildId, CancellationToken); var data = await _guildData.GetData(guildId, CancellationToken);

View file

@ -13,17 +13,29 @@ public static class GuildSettings
public static readonly LanguageOption Language = new("Language", "en"); public static readonly LanguageOption Language = new("Language", "en");
/// <summary> /// <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> /// </summary>
/// <remarks> /// <remarks>
/// <list type="bullet"> /// <list type="bullet">
/// <item>No message will be sent if set to "off", "disable" or "disabled".</item> /// <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> /// </list>
/// </remarks> /// </remarks>
/// <seealso cref="GuildMemberJoinedResponder" /> /// <seealso cref="GuildMemberJoinedResponder" />
public static readonly Option<string> WelcomeMessage = new("WelcomeMessage", "default"); 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> /// <summary>
/// Controls whether or not the <see cref="Messages.Ready" /> message should be sent /// Controls whether or not the <see cref="Messages.Ready" /> message should be sent
/// in <see cref="PrivateFeedbackChannel" /> on startup. /// in <see cref="PrivateFeedbackChannel" /> on startup.
@ -56,9 +68,15 @@ public static class GuildSettings
/// </summary> /// </summary>
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); 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 EventNotificationChannel = new("EventNotificationChannel");
public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
public static readonly SnowflakeOption MuteRole = new("MuteRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole");
public static readonly SnowflakeOption ModeratorRole = new("ModeratorRole");
public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole"); public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole");
/// <summary> /// <summary>

View file

@ -5,10 +5,9 @@ namespace Octobot.Data;
/// </summary> /// </summary>
public sealed class MemberData 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; Id = id;
BannedUntil = bannedUntil;
if (reminders is not null) if (reminders is not null)
{ {
Reminders = reminders; Reminders = reminders;

View file

@ -14,6 +14,7 @@ public enum AllOptionsEnum
{ {
[UsedImplicitly] Language, [UsedImplicitly] Language,
[UsedImplicitly] WelcomeMessage, [UsedImplicitly] WelcomeMessage,
[UsedImplicitly] LeaveMessage,
[UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] ReceiveStartupMessages,
[UsedImplicitly] RemoveRolesOnMute, [UsedImplicitly] RemoveRolesOnMute,
[UsedImplicitly] ReturnRolesOnRejoin, [UsedImplicitly] ReturnRolesOnRejoin,
@ -21,9 +22,11 @@ public enum AllOptionsEnum
[UsedImplicitly] RenameHoistedUsers, [UsedImplicitly] RenameHoistedUsers,
[UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PublicFeedbackChannel,
[UsedImplicitly] PrivateFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel,
[UsedImplicitly] WelcomeMessagesChannel,
[UsedImplicitly] EventNotificationChannel, [UsedImplicitly] EventNotificationChannel,
[UsedImplicitly] DefaultRole, [UsedImplicitly] DefaultRole,
[UsedImplicitly] MuteRole, [UsedImplicitly] MuteRole,
[UsedImplicitly] ModeratorRole,
[UsedImplicitly] EventNotificationRole, [UsedImplicitly] EventNotificationRole,
[UsedImplicitly] EventEarlyNotificationOffset [UsedImplicitly] EventEarlyNotificationOffset
} }

View file

@ -20,7 +20,7 @@ public sealed class BoolOption : Option<bool>
} }
settings[Name] = value; settings[Name] = value;
return Result.FromSuccess(); return Result.Success;
} }
private static bool TryParseBool(string from, out bool value) private static bool TryParseBool(string from, out bool value)

View file

@ -35,7 +35,13 @@ public class Option<T> : IOption
public virtual Result Set(JsonNode settings, string from) public virtual Result Set(JsonNode settings, string from)
{ {
settings[Name] = from; settings[Name] = from;
return Result.FromSuccess(); return Result.Success;
}
public Result Reset(JsonNode settings)
{
settings[Name] = null;
return Result.Success;
} }
/// <summary> /// <summary>
@ -48,10 +54,4 @@ public class Option<T> : IOption
var property = settings[Name]; var property = settings[Name];
return property != null ? property.GetValue<T>() : DefaultValue; return property != null ? property.GetValue<T>() : DefaultValue;
} }
public Result Reset(JsonNode settings)
{
settings[Name] = null;
return Result.FromSuccess();
}
} }

View file

@ -32,7 +32,7 @@ public sealed partial class SnowflakeOption : Option<Snowflake>
} }
settings[Name] = parsed; settings[Name] = parsed;
return Result.FromSuccess(); return Result.Success;
} }
[GeneratedRegex("[^0-9]")] [GeneratedRegex("[^0-9]")]

View file

@ -22,6 +22,6 @@ public sealed class TimeSpanOption : Option<TimeSpan>
} }
settings[Name] = span.ToString(); settings[Name] = span.ToString();
return Result.FromSuccess(); return Result.Success;
} }
} }

View file

@ -20,7 +20,7 @@ public static class ChannelApiExtensions
{ {
if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) 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 }, return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed },

View file

@ -32,7 +32,7 @@ public static class CollectionExtensions
{ {
return list.Count switch return list.Count switch
{ {
0 => Result.FromSuccess(), 0 => Result.Success,
1 => list[0], 1 => list[0],
_ => new AggregateError(list.Cast<IResult>().ToArray()) _ => new AggregateError(list.Cast<IResult>().ToArray())
}; };

View file

@ -13,7 +13,7 @@ public static class FeedbackServiceExtensions
{ {
if (!embedResult.IsDefined(out var embed)) if (!embedResult.IsDefined(out var embed))
{ {
return Result.FromError(embedResult); return ResultExtensions.FromError(embedResult);
} }
return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct); return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct);

View file

@ -22,7 +22,7 @@ public static class GuildScheduledEventExtensions
} }
return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime) return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime)
? Result.FromSuccess() ? Result.Success
: new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)); : new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime));
} }
} }

View file

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

View file

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

View file

@ -66,21 +66,21 @@ namespace Octobot {
} }
} }
internal static string Sound1 { internal static string Generic1 {
get { get {
return ResourceManager.GetString("Sound1", resourceCulture); return ResourceManager.GetString("Generic1", resourceCulture);
} }
} }
internal static string Sound2 { internal static string Generic2 {
get { get {
return ResourceManager.GetString("Sound2", resourceCulture); return ResourceManager.GetString("Generic2", resourceCulture);
} }
} }
internal static string Sound3 { internal static string Generic3 {
get { get {
return ResourceManager.GetString("Sound3", resourceCulture); return ResourceManager.GetString("Generic3", resourceCulture);
} }
} }
@ -959,18 +959,26 @@ namespace Octobot {
} }
} }
internal static string InvalidTimeSpan internal static string DefaultLeaveMessage {
{ get {
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); return ResourceManager.GetString("InvalidTimeSpan", resourceCulture);
} }
} }
internal static string UserInfoKicked internal static string UserInfoKicked {
{ get {
get
{
return ResourceManager.GetString("UserInfoKicked", resourceCulture); return ResourceManager.GetString("UserInfoKicked", resourceCulture);
} }
} }
@ -1106,5 +1114,29 @@ namespace Octobot {
return ResourceManager.GetString("TimeSpanExample", resourceCulture); 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);
}
}
} }
} }

View file

@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Octobot.Attributes;
using Octobot.Commands.Events; using Octobot.Commands.Events;
using Octobot.Services; using Octobot.Services;
using Octobot.Services.Update; using Octobot.Services.Update;
@ -22,16 +23,17 @@ namespace Octobot;
public sealed class 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( public static readonly AllowedMentions NoMentions = new(
Array.Empty<MentionType>(), Array.Empty<Snowflake>(), Array.Empty<Snowflake>()); 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) public static async Task Main(string[] args)
{ {
var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
var services = host.Services; var services = host.Services;
StaticLogger = services.GetRequiredService<ILogger<Octobot>>();
var slashService = services.GetRequiredService<SlashService>(); var slashService = services.GetRequiredService<SlashService>();
// Providing a guild ID to this call will result in command duplicates! // Providing a guild ID to this call will result in command duplicates!
@ -86,8 +88,9 @@ public sealed class Octobot
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>() .AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>() .AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
// Services // Services
.AddSingleton<Utility>() .AddSingleton<AccessControlService>()
.AddSingleton<GuildDataService>() .AddSingleton<GuildDataService>()
.AddSingleton<Utility>()
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>()) .AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
.AddHostedService<MemberUpdateService>() .AddHostedService<MemberUpdateService>()
.AddHostedService<ScheduledEventUpdateService>() .AddHostedService<ScheduledEventUpdateService>()

View file

@ -42,7 +42,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
{ {
if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild
{ {
return Result.FromSuccess(); return Result.Success;
} }
var guild = gatewayEvent.Guild.AsT0; var guild = gatewayEvent.Guild.AsT0;
@ -57,7 +57,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
var botResult = await _userApi.GetCurrentUserAsync(ct); var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot)) if (!botResult.IsDefined(out var bot))
{ {
return Result.FromError(botResult); return ResultExtensions.FromError(botResult);
} }
if (data.DataLoadFailed) if (data.DataLoadFailed)
@ -68,27 +68,23 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct); var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct);
if (!ownerResult.IsDefined(out var owner)) 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", _logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members",
guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount); 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(); return Result.Success;
}
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
{
return Result.FromSuccess();
} }
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
var i = Random.Shared.Next(1, 4); var i = Random.Shared.Next(1, 4);
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot) var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
.WithTitle($"Sound{i}".Localized()) .WithTitle($"Generic{i}".Localized())
.WithDescription(Messages.Ready) .WithDescription(Messages.Ready)
.WithCurrentTimestamp() .WithCurrentTimestamp()
.WithColour(ColorsList.Blue) .WithColour(ColorsList.Blue)
@ -103,7 +99,7 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct);
if (!channelResult.IsDefined(out var channel)) if (!channelResult.IsDefined(out var channel))
{ {
return Result.FromError(channelResult); return ResultExtensions.FromError(channelResult);
} }
var errorEmbed = new EmbedBuilder() var errorEmbed = new EmbedBuilder()
@ -115,9 +111,12 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
var issuesButton = new ButtonComponent( var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link, ButtonComponentStyle.Link,
Messages.ButtonReportIssue, BuildInfo.IsDirty
? Messages.ButtonDirty
: Messages.ButtonReportIssue,
new PartialEmoji(Name: "⚠️"), new PartialEmoji(Name: "⚠️"),
URL: Octobot.IssuesUrl URL: BuildInfo.IssuesUrl,
IsDisabled: BuildInfo.IsDirty
); );
return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed,

View file

@ -48,13 +48,13 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct); var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct);
if (!returnRolesResult.IsSuccess) 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") || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
{ {
return Result.FromSuccess(); return Result.Success;
} }
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
@ -65,7 +65,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
if (!guildResult.IsDefined(out var guild)) if (!guildResult.IsDefined(out var guild))
{ {
return Result.FromError(guildResult); return ResultExtensions.FromError(guildResult);
} }
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
.Build(); .Build();
return await _channelApi.CreateMessageWithEmbedResultAsync( return await _channelApi.CreateMessageWithEmbedResultAsync(
GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed, GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
allowedMentions: Octobot.NoMentions, ct: ct); allowedMentions: Octobot.NoMentions, ct: ct);
} }
@ -85,7 +85,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
{ {
if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg))
{ {
return Result.FromSuccess(); return Result.Success;
} }
var assignRoles = new List<Snowflake>(); var assignRoles = new List<Snowflake>();

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

View file

@ -33,6 +33,6 @@ public class GuildUnloadedResponder : IResponder<IGuildDelete>
_logger.LogInformation("Unloaded guild {GuildId}", guildId); _logger.LogInformation("Unloaded guild {GuildId}", guildId);
} }
return Task.FromResult(Result.FromSuccess()); return Task.FromResult(Result.Success);
} }
} }

View file

@ -39,31 +39,31 @@ public class MessageDeletedResponder : IResponder<IMessageDelete>
{ {
if (!gatewayEvent.GuildID.IsDefined(out var guildId)) if (!gatewayEvent.GuildID.IsDefined(out var guildId))
{ {
return Result.FromSuccess(); return Result.Success;
} }
var cfg = await _guildData.GetSettings(guildId, ct); var cfg = await _guildData.GetSettings(guildId, ct);
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
{ {
return Result.FromSuccess(); return Result.Success;
} }
var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
if (!messageResult.IsDefined(out var message)) if (!messageResult.IsDefined(out var message))
{ {
return Result.FromError(messageResult); return ResultExtensions.FromError(messageResult);
} }
if (string.IsNullOrWhiteSpace(message.Content)) if (string.IsNullOrWhiteSpace(message.Content))
{ {
return Result.FromSuccess(); return Result.Success;
} }
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
if (!auditLogResult.IsDefined(out var auditLogPage)) if (!auditLogResult.IsDefined(out var auditLogPage))
{ {
return Result.FromError(auditLogResult); return ResultExtensions.FromError(auditLogResult);
} }
var auditLog = auditLogPage.AuditLogEntries.Single(); var auditLog = auditLogPage.AuditLogEntries.Single();
@ -78,15 +78,16 @@ public class MessageDeletedResponder : IResponder<IMessageDelete>
if (!deleterResult.IsDefined(out var deleter)) if (!deleterResult.IsDefined(out var deleter))
{ {
return Result.FromError(deleterResult); return ResultExtensions.FromError(deleterResult);
} }
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
var builder = new StringBuilder().AppendLine( var builder = new StringBuilder()
string.Format(Messages.DescriptionActionJumpToChannel, .AppendLine(message.Content.InBlockCode())
Mention.Channel(gatewayEvent.ChannelID))) .AppendLine(
.AppendLine(message.Content.InBlockCode()); string.Format(Messages.DescriptionActionJumpToChannel, Mention.Channel(gatewayEvent.ChannelID))
);
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
.WithSmallTitle( .WithSmallTitle(

View file

@ -46,30 +46,18 @@ public class MessageEditedResponder : IResponder<IMessageUpdate>
return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); 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(); return Result.Success;
}
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();
} }
var cfg = await _guildData.GetSettings(guildId, ct); 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); var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
@ -78,12 +66,12 @@ public class MessageEditedResponder : IResponder<IMessageUpdate>
if (!messageResult.IsDefined(out var message)) if (!messageResult.IsDefined(out var message))
{ {
_ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
return Result.FromSuccess(); return Result.Success;
} }
if (message.Content == newContent) if (message.Content == newContent)
{ {
return Result.FromSuccess(); return Result.Success;
} }
// Custom event responders are called earlier than responders responsible for message caching // 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); Messages.Culture = GuildSettings.Language.Get(cfg);
var builder = new StringBuilder().AppendLine( var builder = new StringBuilder()
string.Format(Messages.DescriptionActionJumpToMessage, .AppendLine(diff.AsMarkdown())
$"https://discord.com/channels/{guildId}/{channelId}/{messageId}")) .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage,
.AppendLine(diff.AsMarkdown()); $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")
);
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)

View file

@ -34,6 +34,6 @@ public class MessageCreateResponder : IResponder<IMessageCreate>
"лан" => "https://i.ibb.co/VYH2QLc/lan.jpg", "лан" => "https://i.ibb.co/VYH2QLc/lan.jpg",
_ => default(Optional<string>) _ => default(Optional<string>)
}); });
return Task.FromResult(Result.FromSuccess()); return Task.FromResult(Result.Success);
} }
} }

View file

@ -0,0 +1,176 @@
using Octobot.Data;
using Octobot.Extensions;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Results;
using Remora.Rest.Core;
using Remora.Results;
namespace Octobot.Services;
public sealed class AccessControlService
{
private readonly GuildDataService _data;
private readonly IDiscordRestGuildAPI _guildApi;
private readonly RequireDiscordPermissionCondition _permission;
private readonly IDiscordRestUserAPI _userApi;
public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
RequireDiscordPermissionCondition permission)
{
_data = data;
_guildApi = guildApi;
_userApi = userApi;
_permission = permission;
}
private async Task<Result<bool>> CheckPermissionAsync(GuildData data, Snowflake memberId, IGuildMember member,
DiscordPermission permission, CancellationToken ct = default)
{
var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings);
var result = await _permission.CheckAsync(new RequireDiscordPermissionAttribute([permission]), member, ct);
if (result.Error is not null and not PermissionDeniedError)
{
return Result<bool>.FromError(result);
}
var hasPermission = result.IsSuccess;
return hasPermission || (!moderatorRole.Empty() &&
data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value));
}
/// <summary>
/// Checks whether or not a member can interact with another member
/// </summary>
/// <param name="guildId">The ID of the guild in which an operation is being performed.</param>
/// <param name="interacterId">The executor of the operation.</param>
/// <param name="targetId">The target of the operation.</param>
/// <param name="action">The operation.</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>
/// <list type="bullet">
/// <item>A result which has succeeded with a null string if the member can interact with the target.</item>
/// <item>
/// A result which has succeeded with a non-null string containing the error message if the member cannot
/// interact with the target.
/// </item>
/// <item>A result which has failed if an error occurred during the execution of this method.</item>
/// </list>
/// </returns>
public async Task<Result<string?>> CheckInteractionsAsync(
Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
{
if (interacterId == targetId)
{
return Result<string?>.FromSuccess($"UserCannot{action}Themselves".Localized());
}
var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot))
{
return Result<string?>.FromError(botResult);
}
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
if (!guildResult.IsDefined(out var guild))
{
return Result<string?>.FromError(guildResult);
}
var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct);
if (!targetMemberResult.IsDefined(out var targetMember))
{
return Result<string?>.FromSuccess(null);
}
var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
if (!botMemberResult.IsDefined(out var botMember))
{
return Result<string?>.FromError(botMemberResult);
}
var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
if (!rolesResult.IsDefined(out var roles))
{
return Result<string?>.FromError(rolesResult);
}
if (interacterId is null)
{
return CheckInteractions(action, guild, roles, targetMember, botMember, botMember);
}
var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct);
if (!interacterResult.IsDefined(out var interacter))
{
return Result<string?>.FromError(interacterResult);
}
var data = await _data.GetData(guildId, ct);
var permissionResult = await CheckPermissionAsync(data, interacterId.Value, interacter,
action switch
{
"Ban" => DiscordPermission.BanMembers,
"Kick" => DiscordPermission.KickMembers,
"Mute" or "Unmute" => DiscordPermission.ModerateMembers,
_ => throw new Exception()
}, ct);
if (!permissionResult.IsDefined(out var hasPermission))
{
return Result<string?>.FromError(permissionResult);
}
return hasPermission
? CheckInteractions(action, guild, roles, targetMember, botMember, interacter)
: Result<string?>.FromSuccess($"UserCannot{action}Members".Localized());
}
private static Result<string?> CheckInteractions(
string action, IGuild guild, IReadOnlyList<IRole> roles, IGuildMember targetMember, IGuildMember botMember,
IGuildMember interacter)
{
if (!targetMember.User.IsDefined(out var targetUser))
{
return new ArgumentNullError(nameof(targetMember.User));
}
if (!interacter.User.IsDefined(out var interacterUser))
{
return new ArgumentNullError(nameof(interacter.User));
}
if (botMember.User == targetMember.User)
{
return Result<string?>.FromSuccess($"UserCannot{action}Bot".Localized());
}
if (targetUser.ID == guild.OwnerID)
{
return Result<string?>.FromSuccess($"UserCannot{action}Owner".Localized());
}
var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList();
var botRoles = roles.Where(r => botMember.Roles.Contains(r.ID));
var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position);
if (targetBotRoleDiff >= 0)
{
return Result<string?>.FromSuccess($"BotCannot{action}Target".Localized());
}
if (interacterUser.ID == guild.OwnerID)
{
return Result<string?>.FromSuccess(null);
}
var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID));
var targetInteracterRoleDiff
= targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position);
return targetInteracterRoleDiff < 0
? Result<string?>.FromSuccess(null)
: Result<string?>.FromSuccess($"UserCannot{action}Target".Localized());
}
}

View file

@ -26,20 +26,20 @@ public sealed partial class MemberUpdateService : BackgroundService
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag" "Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
]; ];
private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData; private readonly GuildDataService _guildData;
private readonly ILogger<MemberUpdateService> _logger; private readonly ILogger<MemberUpdateService> _logger;
private readonly Utility _utility;
public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi, public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi,
GuildDataService guildData, ILogger<MemberUpdateService> logger, Utility utility) IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger<MemberUpdateService> logger)
{ {
_access = access;
_channelApi = channelApi; _channelApi = channelApi;
_guildApi = guildApi; _guildApi = guildApi;
_guildData = guildData; _guildData = guildData;
_logger = logger; _logger = logger;
_utility = utility;
} }
protected override async Task ExecuteAsync(CancellationToken ct) protected override async Task ExecuteAsync(CancellationToken ct)
@ -94,10 +94,10 @@ public sealed partial class MemberUpdateService : BackgroundService
} }
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct); = await _access.CheckInteractionsAsync(guildId, null, id, "Update", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
{ {
return Result.FromError(interactionResult); return ResultExtensions.FromError(interactionResult);
} }
var canInteract = interactionResult.Entity is null; var canInteract = interactionResult.Entity is null;
@ -121,7 +121,7 @@ public sealed partial class MemberUpdateService : BackgroundService
if (!canInteract) if (!canInteract)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct); 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) 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( var unbanResult = await _guildApi.RemoveGuildBanAsync(
@ -166,7 +173,7 @@ public sealed partial class MemberUpdateService : BackgroundService
{ {
if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var unmuteResult = await _guildApi.ModifyGuildMemberAsync( var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
@ -202,7 +209,7 @@ public sealed partial class MemberUpdateService : BackgroundService
if (!usernameChanged) if (!usernameChanged)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var newNickname = string.Concat(characterList.ToArray()); var newNickname = string.Concat(characterList.ToArray());
@ -223,12 +230,13 @@ public sealed partial class MemberUpdateService : BackgroundService
{ {
if (DateTimeOffset.UtcNow < reminder.At) if (DateTimeOffset.UtcNow < reminder.At)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var builder = new StringBuilder() var builder = new StringBuilder()
.AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) .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( var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.Reminder, user.GetTag()), user) 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); reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct);
if (!messageResult.IsSuccess) if (!messageResult.IsSuccess)
{ {
return messageResult; return ResultExtensions.FromError(messageResult);
} }
data.Reminders.Remove(reminder); data.Reminders.Remove(reminder);
return Result.FromSuccess(); return Result.Success;
} }
} }

View file

@ -53,7 +53,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
if (!eventsResult.IsDefined(out var events)) if (!eventsResult.IsDefined(out var events))
{ {
return Result.FromError(eventsResult); return ResultExtensions.FromError(eventsResult);
} }
SyncScheduledEvents(data, events); SyncScheduledEvents(data, events);
@ -147,7 +147,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|| eventData.EarlyNotificationSent || eventData.EarlyNotificationSent
|| DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset) || DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset)
{ {
return Result.FromSuccess(); return Result.Success;
} }
var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct); var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct);
@ -182,7 +182,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
{ {
if (GuildSettings.EventNotificationChannel.Get(settings).Empty()) if (GuildSettings.EventNotificationChannel.Get(settings).Empty())
{ {
return Result.FromSuccess(); return Result.Success;
} }
if (!scheduledEvent.Creator.IsDefined(out var creator)) if (!scheduledEvent.Creator.IsDefined(out var creator))
@ -204,7 +204,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (!embedDescriptionResult.IsDefined(out var embedDescription)) if (!embedDescriptionResult.IsDefined(out var embedDescription))
{ {
return Result.FromError(embedDescriptionResult); return ResultExtensions.FromError(embedDescriptionResult);
} }
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
@ -283,7 +283,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{ {
return Result.FromSuccess(); return Result.Success;
} }
var embedDescriptionResult = scheduledEvent.EntityType switch var embedDescriptionResult = scheduledEvent.EntityType switch
@ -298,12 +298,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService
scheduledEvent, data, ct); scheduledEvent, data, ct);
if (!contentResult.IsDefined(out var content)) if (!contentResult.IsDefined(out var content))
{ {
return Result.FromError(contentResult); return ResultExtensions.FromError(contentResult);
} }
if (!embedDescriptionResult.IsDefined(out var embedDescription)) if (!embedDescriptionResult.IsDefined(out var embedDescription))
{ {
return Result.FromError(embedDescriptionResult); return ResultExtensions.FromError(embedDescriptionResult);
} }
var startedEmbed = new EmbedBuilder() var startedEmbed = new EmbedBuilder()
@ -324,7 +324,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{ {
data.ScheduledEvents.Remove(eventData.Id); data.ScheduledEvents.Remove(eventData.Id);
return Result.FromSuccess(); return Result.Success;
} }
var completedEmbed = new EmbedBuilder() var completedEmbed = new EmbedBuilder()
@ -356,7 +356,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{ {
data.ScheduledEvents.Remove(eventData.Id); data.ScheduledEvents.Remove(eventData.Id);
return Result.FromSuccess(); return Result.Success;
} }
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
@ -409,14 +409,14 @@ public sealed class ScheduledEventUpdateService : BackgroundService
{ {
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{ {
return Result.FromSuccess(); return Result.Success;
} }
var contentResult = await _utility.GetEventNotificationMentions( var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data, ct); scheduledEvent, data, ct);
if (!contentResult.IsDefined(out var content)) if (!contentResult.IsDefined(out var content))
{ {
return Result.FromError(contentResult); return ResultExtensions.FromError(contentResult);
} }
var earlyResult = new EmbedBuilder() var earlyResult = new EmbedBuilder()

View file

@ -21,129 +21,13 @@ public sealed class Utility
private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
private readonly IDiscordRestGuildAPI _guildApi; private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi;
public Utility( public Utility(
IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi, IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi)
IDiscordRestUserAPI userApi)
{ {
_channelApi = channelApi; _channelApi = channelApi;
_eventApi = eventApi; _eventApi = eventApi;
_guildApi = guildApi; _guildApi = guildApi;
_userApi = userApi;
}
/// <summary>
/// Checks whether or not a member can interact with another member
/// </summary>
/// <param name="guildId">The ID of the guild in which an operation is being performed.</param>
/// <param name="interacterId">The executor of the operation.</param>
/// <param name="targetId">The target of the operation.</param>
/// <param name="action">The operation.</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>
/// <list type="bullet">
/// <item>A result which has succeeded with a null string if the member can interact with the target.</item>
/// <item>
/// A result which has succeeded with a non-null string containing the error message if the member cannot
/// interact with the target.
/// </item>
/// <item>A result which has failed if an error occurred during the execution of this method.</item>
/// </list>
/// </returns>
public async Task<Result<string?>> CheckInteractionsAsync(
Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
{
if (interacterId == targetId)
{
return Result<string?>.FromSuccess($"UserCannot{action}Themselves".Localized());
}
var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot))
{
return Result<string?>.FromError(botResult);
}
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
if (!guildResult.IsDefined(out var guild))
{
return Result<string?>.FromError(guildResult);
}
var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct);
if (!targetMemberResult.IsDefined(out var targetMember))
{
return Result<string?>.FromSuccess(null);
}
var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
if (!currentMemberResult.IsDefined(out var currentMember))
{
return Result<string?>.FromError(currentMemberResult);
}
var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
if (!rolesResult.IsDefined(out var roles))
{
return Result<string?>.FromError(rolesResult);
}
if (interacterId is null)
{
return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember);
}
var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct);
return interacterResult.IsDefined(out var interacter)
? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter)
: Result<string?>.FromError(interacterResult);
}
private static Result<string?> CheckInteractions(
string action, IGuild guild, IReadOnlyList<IRole> roles, IGuildMember targetMember, IGuildMember currentMember,
IGuildMember interacter)
{
if (!targetMember.User.IsDefined(out var targetUser))
{
return new ArgumentNullError(nameof(targetMember.User));
}
if (!interacter.User.IsDefined(out var interacterUser))
{
return new ArgumentNullError(nameof(interacter.User));
}
if (currentMember.User == targetMember.User)
{
return Result<string?>.FromSuccess($"UserCannot{action}Bot".Localized());
}
if (targetUser.ID == guild.OwnerID)
{
return Result<string?>.FromSuccess($"UserCannot{action}Owner".Localized());
}
var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList();
var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID));
var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position);
if (targetBotRoleDiff >= 0)
{
return Result<string?>.FromSuccess($"BotCannot{action}Target".Localized());
}
if (interacterUser.ID == guild.OwnerID)
{
return Result<string?>.FromSuccess(null);
}
var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID));
var targetInteracterRoleDiff
= targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position);
return targetInteracterRoleDiff < 0
? Result<string?>.FromSuccess(null)
: Result<string?>.FromSuccess($"UserCannot{action}Target".Localized());
} }
/// <summary> /// <summary>