1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-01-31 09:09:00 +03:00

Force maximum Cognitive Complexity via InspectCode (#60)

This PR uses a new feature of the InspectCode action: extensions. By
adding the `Cognitive Complexity` extension, InspectCode can provide
warnings and force the contributors to follow standards regarding
complex methods. This functionality was previously provided by
CodeFactor, but it is no longer used. Here's the full changelog of this
PR:
- Allowed Actions to run on push for any branch, not just `master`;
- Added `Cognitive Complexity` plugin for InspectCode;
- Significantly reduced complexity of KickCommandGroup, MuteCommandGroup
and GuildUpdateService

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
Octol1ttle 2023-07-21 01:41:02 +05:00
parent 8c72dc663e
commit 27b8f15e3b
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
11 changed files with 476 additions and 421 deletions

View file

@ -4,12 +4,12 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
on: on:
push:
branches: [ "*" ]
pull_request: pull_request:
branches: [ "master" ] branches: [ "master" ]
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
push:
branches: [ "master" ]
jobs: jobs:
inspect-code: inspect-code:
@ -32,4 +32,5 @@ jobs:
with: with:
solutionPath: ./Boyfriend.sln solutionPath: ./Boyfriend.sln
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement
extensions: ReSharperPlugin.CognitiveComplexity
solutionWideAnalysis: true solutionWideAnalysis: true

View file

@ -5,6 +5,7 @@ using Boyfriend.Services;
using JetBrains.Annotations; using JetBrains.Annotations;
using Remora.Commands.Attributes; using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Conditions;
@ -47,7 +48,7 @@ public class AboutCommandGroup : CommandGroup {
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[Description("Shows Boyfriend's developers")] [Description("Shows Boyfriend's developers")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> SendAboutBotAsync() { public async Task<Result> ExecuteAboutAsync() {
if (!_context.TryGetContextIDs(out var guildId, out _, out _)) if (!_context.TryGetContextIDs(out var guildId, out _, out _))
return Result.FromError( return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
@ -59,6 +60,10 @@ public class AboutCommandGroup : CommandGroup {
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
return await SendAboutBotAsync(currentUser, CancellationToken);
}
private async Task<Result> SendAboutBotAsync(IUser currentUser, CancellationToken ct = default) {
var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers)); var builder = new StringBuilder().AppendLine(Markdown.Bold(Messages.AboutTitleDevelopers));
foreach (var dev in Developers) foreach (var dev in Developers)
builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}"); builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}");
@ -73,6 +78,6 @@ public class AboutCommandGroup : CommandGroup {
.WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png") .WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png")
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -66,7 +66,7 @@ public class BanCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("Ban user")] [Description("Ban user")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> ExecuteBan( public async Task<Result> ExecuteBanAsync(
[Description("User to ban")] IUser target, [Description("User to ban")] IUser target,
[Description("Ban reason")] string reason, [Description("Ban reason")] string reason,
[Description("Ban duration")] TimeSpan? duration = null) { [Description("Ban duration")] TimeSpan? duration = null) {
@ -84,26 +84,26 @@ public class BanCommandGroup : CommandGroup {
if (!guildResult.IsDefined(out var guild)) if (!guildResult.IsDefined(out var guild))
return Result.FromError(guildResult); return Result.FromError(guildResult);
return await BanUserAsync(target, reason, duration, guild, channelId.Value, user, currentUser); var data = await _dataService.GetData(guild.ID, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
return await BanUserAsync(
target, reason, duration, guild, data, channelId.Value, user, currentUser, CancellationToken);
} }
private async Task<Result> BanUserAsync( private async Task<Result> BanUserAsync(
IUser target, string reason, TimeSpan? duration, IGuild guild, Snowflake channelId, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId,
IUser user, IUser currentUser) { IUser user, IUser currentUser, CancellationToken ct = default) {
var data = await _dataService.GetData(guild.ID, CancellationToken); var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
var cfg = data.Settings;
Messages.Culture = GuildSettings.Language.Get(cfg);
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, CancellationToken);
if (existingBanResult.IsDefined()) { if (existingBanResult.IsDefined()) {
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
} }
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", CancellationToken); = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Ban", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult); return Result.FromError(interactionResult);
@ -111,7 +111,7 @@ public class BanCommandGroup : CommandGroup {
var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct);
} }
var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason)); var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason));
@ -123,7 +123,7 @@ public class BanCommandGroup : CommandGroup {
var title = string.Format(Messages.UserBanned, target.GetTag()); var title = string.Format(Messages.UserBanned, target.GetTag());
var description = builder.ToString(); var description = builder.ToString();
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
if (dmChannelResult.IsDefined(out var dmChannel)) { if (dmChannelResult.IsDefined(out var dmChannel)) {
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereBanned) .WithTitle(Messages.YouWereBanned)
@ -135,12 +135,12 @@ public class BanCommandGroup : CommandGroup {
if (!dmEmbed.IsDefined(out var dmBuilt)) if (!dmEmbed.IsDefined(out var dmBuilt))
return Result.FromError(dmEmbed); return Result.FromError(dmEmbed);
await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct);
} }
var banResult = await _guildApi.CreateGuildBanAsync( var banResult = await _guildApi.CreateGuildBanAsync(
guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken); ct: ct);
if (!banResult.IsSuccess) if (!banResult.IsSuccess)
return Result.FromError(banResult.Error); return Result.FromError(banResult.Error);
var memberData = data.GetMemberData(target.ID); var memberData = data.GetMemberData(target.ID);
@ -152,11 +152,12 @@ public class BanCommandGroup : CommandGroup {
title, target) title, target)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); var logResult = _utility.LogActionAsync(
data.Settings, channelId, user, title, description, target, ct);
if (!logResult.IsSuccess) if (!logResult.IsSuccess)
return Result.FromError(logResult.Error); return Result.FromError(logResult.Error);
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
/// <summary> /// <summary>
@ -171,7 +172,7 @@ public class BanCommandGroup : CommandGroup {
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
/// was unbanned and vice-versa. /// was unbanned and vice-versa.
/// </returns> /// </returns>
/// <seealso cref="ExecuteBan" /> /// <seealso cref="ExecuteBanAsync" />
/// <seealso cref="GuildUpdateService.TickGuildAsync"/> /// <seealso cref="GuildUpdateService.TickGuildAsync"/>
[Command("unban")] [Command("unban")]
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
@ -196,25 +197,27 @@ public class BanCommandGroup : CommandGroup {
if (!userResult.IsDefined(out var user)) if (!userResult.IsDefined(out var user))
return Result.FromError(userResult); return Result.FromError(userResult);
return await UnbanUserAsync(target, reason, guildId.Value, channelId.Value, user, currentUser); var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
return await UnbanUserAsync(
target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken);
} }
private async Task<Result> UnbanUserAsync( private async Task<Result> UnbanUserAsync(
IUser target, string reason, Snowflake guildId, Snowflake channelId, IUser user, IUser currentUser) { IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user,
var cfg = await _dataService.GetSettings(guildId, CancellationToken); IUser currentUser, CancellationToken ct = default) {
Messages.Culture = GuildSettings.Language.Get(cfg); var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct);
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, CancellationToken);
if (!existingBanResult.IsDefined()) { if (!existingBanResult.IsDefined()) {
var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser) var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct);
} }
var unbanResult = await _guildApi.RemoveGuildBanAsync( var unbanResult = await _guildApi.RemoveGuildBanAsync(
guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken); ct);
if (!unbanResult.IsSuccess) if (!unbanResult.IsSuccess)
return Result.FromError(unbanResult.Error); return Result.FromError(unbanResult.Error);
@ -224,10 +227,10 @@ public class BanCommandGroup : CommandGroup {
var title = string.Format(Messages.UserUnbanned, target.GetTag()); var title = string.Format(Messages.UserUnbanned, target.GetTag());
var description = string.Format(Messages.DescriptionActionReason, reason); var description = string.Format(Messages.DescriptionActionReason, reason);
var logResult = _utility.LogActionAsync(cfg, channelId, user, title, description, target, CancellationToken); var logResult = _utility.LogActionAsync(data.Settings, channelId, user, title, description, target, ct);
if (!logResult.IsSuccess) if (!logResult.IsSuccess)
return Result.FromError(logResult.Error); return Result.FromError(logResult.Error);
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -76,15 +76,15 @@ public class ClearCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
return await ClearMessagesAsync(amount, guildId.Value, channelId.Value, messages, user, currentUser); var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
return await ClearMessagesAsync(amount, data, channelId.Value, messages, user, currentUser, CancellationToken);
} }
private async Task<Result> ClearMessagesAsync( private async Task<Result> ClearMessagesAsync(
int amount, Snowflake guildId, Snowflake channelId, IReadOnlyList<IMessage> messages, int amount, GuildData data, Snowflake channelId, IReadOnlyList<IMessage> messages,
IUser user, IUser currentUser) { IUser user, IUser currentUser, CancellationToken ct = default) {
var cfg = await _dataService.GetSettings(guildId, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg);
var idList = new List<Snowflake>(messages.Count); var idList = new List<Snowflake>(messages.Count);
var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine(); var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine();
for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...') for (var i = messages.Count - 1; i >= 1; i--) { // '>= 1' to skip last message ('Boyfriend is thinking...')
@ -98,18 +98,18 @@ public class ClearCommandGroup : CommandGroup {
var description = builder.ToString(); var description = builder.ToString();
var deleteResult = await _channelApi.BulkDeleteMessagesAsync( var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId, idList, user.GetTag().EncodeHeader(), CancellationToken); channelId, idList, user.GetTag().EncodeHeader(), ct);
if (!deleteResult.IsSuccess) if (!deleteResult.IsSuccess)
return Result.FromError(deleteResult.Error); return Result.FromError(deleteResult.Error);
var logResult = _utility.LogActionAsync( var logResult = _utility.LogActionAsync(
cfg, channelId, user, title, description, currentUser, CancellationToken); data.Settings, channelId, user, title, description, currentUser, ct);
if (!logResult.IsSuccess) if (!logResult.IsSuccess)
return Result.FromError(logResult.Error); return Result.FromError(logResult.Error);
var embed = new EmbedBuilder().WithSmallTitle(title, currentUser) var embed = new EmbedBuilder().WithSmallTitle(title, currentUser)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -6,12 +6,12 @@ using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -62,21 +62,25 @@ public class KickCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
[Description("Kick member")] [Description("Kick member")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> KickUserAsync( public async Task<Result> ExecuteKick(
[Description("Member to kick")] IUser target, [Description("Member to kick")] IUser target,
[Description("Kick reason")] string reason) { [Description("Kick reason")] string reason) {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId))
return Result.FromError( return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
// The current user's avatar is used when sending error messages // The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild))
return Result.FromError(guildResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken); var data = await _dataService.GetData(guildId.Value, CancellationToken);
var cfg = data.Settings; Messages.Culture = GuildSettings.Language.Get(data.Settings);
Messages.Culture = GuildSettings.Language.Get(cfg);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) { if (!memberResult.IsSuccess) {
@ -86,26 +90,26 @@ public class KickCommandGroup : CommandGroup {
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
} }
return await KickUserAsync(target, reason, guild, channelId.Value, data, user, currentUser, CancellationToken);
}
private async Task<Result> KickUserAsync(
IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser user, IUser currentUser,
CancellationToken ct = default) {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Kick", CancellationToken); = await _utility.CheckInteractionsAsync(guild.ID, user.ID, target.ID, "Kick", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult); return Result.FromError(interactionResult);
Result<Embed> responseEmbed;
if (interactionResult.Entity is not null) { if (interactionResult.Entity is not null) {
responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();
} else {
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
}
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
if (dmChannelResult.IsDefined(out var dmChannel)) { if (dmChannelResult.IsDefined(out var dmChannel)) {
var guildResult = await _guildApi.GetGuildAsync(guildId.Value, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild))
return Result.FromError(guildResult);
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereKicked) .WithTitle(Messages.YouWereKicked)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason)) .WithDescription(string.Format(Messages.DescriptionActionReason, reason))
@ -116,49 +120,27 @@ public class KickCommandGroup : CommandGroup {
if (!dmEmbed.IsDefined(out var dmBuilt)) if (!dmEmbed.IsDefined(out var dmBuilt))
return Result.FromError(dmEmbed); return Result.FromError(dmEmbed);
await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken); await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct);
} }
var kickResult = await _guildApi.RemoveGuildMemberAsync( var kickResult = await _guildApi.RemoveGuildMemberAsync(
guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), guild.ID, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken); ct);
if (!kickResult.IsSuccess) if (!kickResult.IsSuccess)
return Result.FromError(kickResult.Error); return Result.FromError(kickResult.Error);
data.GetMemberData(target.ID).Roles.Clear(); data.GetMemberData(target.ID).Roles.Clear();
responseEmbed = new EmbedBuilder().WithSmallTitle( var title = string.Format(Messages.UserKicked, target.GetTag());
var description = string.Format(Messages.DescriptionActionReason, reason);
var logResult = _utility.LogActionAsync(
data.Settings, channelId, user, title, description, target, ct);
if (!logResult.IsSuccess)
return Result.FromError(logResult.Error);
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserKicked, target.GetTag()), target) string.Format(Messages.UserKicked, target.GetTag()), target)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
var logEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserKicked, target.GetTag()), target)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
}
}
return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken);
} }
} }

View file

@ -7,13 +7,13 @@ using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting; using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -23,7 +23,6 @@ namespace Boyfriend.Commands;
/// </summary> /// </summary>
[UsedImplicitly] [UsedImplicitly]
public class MuteCommandGroup : CommandGroup { public class MuteCommandGroup : CommandGroup {
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context; private readonly ICommandContext _context;
private readonly GuildDataService _dataService; private readonly GuildDataService _dataService;
private readonly FeedbackService _feedbackService; private readonly FeedbackService _feedbackService;
@ -32,11 +31,9 @@ public class MuteCommandGroup : CommandGroup {
private readonly UtilityService _utility; private readonly UtilityService _utility;
public MuteCommandGroup( public MuteCommandGroup(
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, ICommandContext context, GuildDataService dataService, FeedbackService feedbackService,
FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) {
UtilityService utility) {
_context = context; _context = context;
_channelApi = channelApi;
_dataService = dataService; _dataService = dataService;
_feedbackService = feedbackService; _feedbackService = feedbackService;
_guildApi = guildApi; _guildApi = guildApi;
@ -57,7 +54,7 @@ public class MuteCommandGroup : CommandGroup {
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
/// was muted and vice-versa. /// was muted and vice-versa.
/// </returns> /// </returns>
/// <seealso cref="UnmuteUserAsync" /> /// <seealso cref="ExecuteUnmute" />
[Command("mute", "мут")] [Command("mute", "мут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
@ -66,7 +63,7 @@ public class MuteCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Mute member")] [Description("Mute member")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> MuteUserAsync( public async Task<Result> ExecuteMute(
[Description("Member to mute")] IUser target, [Description("Member to mute")] IUser target,
[Description("Mute reason")] string reason, [Description("Mute reason")] string reason,
[Description("Mute duration")] TimeSpan duration) { [Description("Mute duration")] TimeSpan duration) {
@ -79,6 +76,13 @@ public class MuteCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) { if (!memberResult.IsSuccess) {
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser) var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser)
@ -87,71 +91,49 @@ public class MuteCommandGroup : CommandGroup {
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
} }
return await MuteUserAsync(
target, reason, duration, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken);
}
private async Task<Result> MuteUserAsync(
IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId,
IUser user, IUser currentUser, CancellationToken ct = default) {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync( = await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Mute", CancellationToken); guildId, user.ID, target.ID, "Mute", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult); return Result.FromError(interactionResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
var cfg = data.Settings;
Messages.Culture = GuildSettings.Language.Get(cfg);
Result<Embed> responseEmbed;
if (interactionResult.Entity is not null) { if (interactionResult.Entity is not null) {
responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build(); .WithColour(ColorsList.Red).Build();
} else {
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
if (!userResult.IsDefined(out var user)) }
return Result.FromError(userResult);
var until = DateTimeOffset.UtcNow.Add(duration); // >:) var until = DateTimeOffset.UtcNow.Add(duration); // >:)
var muteResult = await _guildApi.ModifyGuildMemberAsync( var muteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(), guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: until, ct: CancellationToken); communicationDisabledUntil: until, ct: ct);
if (!muteResult.IsSuccess) if (!muteResult.IsSuccess)
return Result.FromError(muteResult.Error); return Result.FromError(muteResult.Error);
responseEmbed = new EmbedBuilder().WithSmallTitle( var title = string.Format(Messages.UserMuted, target.GetTag());
var description = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
.Append(
string.Format(
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
var logResult = _utility.LogActionAsync(
data.Settings, channelId, user, title, description, target, ct);
if (!logResult.IsSuccess)
return Result.FromError(logResult.Error);
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserMuted, target.GetTag()), target) string.Format(Messages.UserMuted, target.GetTag()), target)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
var builder = new StringBuilder().AppendLine(string.Format(Messages.DescriptionActionReason, reason))
.Append(
string.Format(
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until)));
var logEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserMuted, target.GetTag()), target)
.WithDescription(builder.ToString())
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
}
}
return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken);
} }
/// <summary> /// <summary>
@ -166,7 +148,7 @@ public class MuteCommandGroup : CommandGroup {
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
/// was unmuted and vice-versa. /// was unmuted and vice-versa.
/// </returns> /// </returns>
/// <seealso cref="MuteUserAsync" /> /// <seealso cref="ExecuteMute" />
/// <seealso cref="GuildUpdateService.TickGuildAsync"/> /// <seealso cref="GuildUpdateService.TickGuildAsync"/>
[Command("unmute", "размут")] [Command("unmute", "размут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)] [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
@ -176,7 +158,7 @@ public class MuteCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)] [RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Unmute member")] [Description("Unmute member")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> UnmuteUserAsync( public async Task<Result> ExecuteUnmute(
[Description("Member to unmute")] IUser target, [Description("Member to unmute")] IUser target,
[Description("Unmute reason")] string reason) { [Description("Unmute reason")] string reason) {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId)) if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId))
@ -188,8 +170,13 @@ public class MuteCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); // Needed to get the tag and avatar
Messages.Culture = GuildSettings.Language.Get(cfg); var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken); var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) { if (!memberResult.IsSuccess) {
@ -199,56 +186,43 @@ public class MuteCommandGroup : CommandGroup {
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
} }
return await UnmuteUserAsync(
target, reason, guildId.Value, data, channelId.Value, user, currentUser, CancellationToken);
}
private async Task<Result> UnmuteUserAsync(
IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user,
IUser currentUser, CancellationToken ct = default) {
var interactionResult var interactionResult
= await _utility.CheckInteractionsAsync( = await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Unmute", CancellationToken); guildId, user.ID, target.ID, "Unmute", ct);
if (!interactionResult.IsSuccess) if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult); return Result.FromError(interactionResult);
// Needed to get the tag and avatar if (interactionResult.Entity is not null) {
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
if (!userResult.IsDefined(out var user)) .WithColour(ColorsList.Red).Build();
return Result.FromError(userResult);
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
}
var unmuteResult = await _guildApi.ModifyGuildMemberAsync( var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(), guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: null, ct: CancellationToken); communicationDisabledUntil: null, ct: ct);
if (!unmuteResult.IsSuccess) if (!unmuteResult.IsSuccess)
return Result.FromError(unmuteResult.Error); return Result.FromError(unmuteResult.Error);
var responseEmbed = new EmbedBuilder().WithSmallTitle( var title = string.Format(Messages.UserUnmuted, target.GetTag());
var description = string.Format(Messages.DescriptionActionReason, reason);
var logResult = _utility.LogActionAsync(
data.Settings, channelId, user, title, description, target, ct);
if (!logResult.IsSuccess)
return Result.FromError(logResult.Error);
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserUnmuted, target.GetTag()), target) string.Format(Messages.UserUnmuted, target.GetTag()), target)
.WithColour(ColorsList.Green).Build(); .WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
&& GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
|| (!GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)) {
var logEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserUnmuted, target.GetTag()), target)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Green)
.Build();
if (!logEmbed.IsDefined(out var logBuilt))
return Result.FromError(logEmbed);
var builtArray = new[] { logBuilt };
// Not awaiting to reduce response time
if (GuildSettings.PublicFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
if (GuildSettings.PrivateFeedbackChannel.Get(cfg) != GuildSettings.PublicFeedbackChannel.Get(cfg)
&& GuildSettings.PrivateFeedbackChannel.Get(cfg) != channelId.Value)
_ = _channelApi.CreateMessageAsync(
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: builtArray,
ct: CancellationToken);
}
return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken);
} }
} }

View file

@ -4,6 +4,7 @@ using Boyfriend.Services;
using JetBrains.Annotations; using JetBrains.Annotations;
using Remora.Commands.Attributes; using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Conditions;
@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway; using Remora.Discord.Gateway;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -49,7 +51,7 @@ public class PingCommandGroup : CommandGroup {
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> SendPingAsync() { public async Task<Result> ExecutePingAsync() {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _)) if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _))
return Result.FromError( return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
@ -61,11 +63,16 @@ public class PingCommandGroup : CommandGroup {
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
return await SendLatencyAsync(channelId.Value, currentUser, CancellationToken);
}
private async Task<Result> SendLatencyAsync(
Snowflake channelId, IUser currentUser, CancellationToken ct = default) {
var latency = _client.Latency.TotalMilliseconds; var latency = _client.Latency.TotalMilliseconds;
if (latency is 0) { if (latency is 0) {
// No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message // No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message
var lastMessageResult = await _channelApi.GetChannelMessagesAsync( var lastMessageResult = await _channelApi.GetChannelMessagesAsync(
channelId.Value, limit: 1, ct: CancellationToken); channelId, limit: 1, ct: ct);
if (!lastMessageResult.IsDefined(out var lastMessage)) if (!lastMessageResult.IsDefined(out var lastMessage))
return Result.FromError(lastMessageResult); return Result.FromError(lastMessageResult);
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds; latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
@ -78,6 +85,6 @@ public class PingCommandGroup : CommandGroup {
.WithCurrentTimestamp() .WithCurrentTimestamp()
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -4,6 +4,7 @@ using Boyfriend.Services;
using JetBrains.Annotations; using JetBrains.Annotations;
using Remora.Commands.Attributes; using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes; using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Conditions;
@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting; using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Boyfriend.Commands; namespace Boyfriend.Commands;
@ -45,7 +47,7 @@ public class RemindCommandGroup : CommandGroup {
[DiscordDefaultDMPermission(false)] [DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)] [RequireContext(ChannelContext.Guild)]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> AddReminderAsync( public async Task<Result> ExecuteReminderAsync(
[Description("After what period of time mention the reminder")] [Description("After what period of time mention the reminder")]
TimeSpan @in, TimeSpan @in,
[Description("Reminder message")] string message) { [Description("Reminder message")] string message) {
@ -57,12 +59,21 @@ public class RemindCommandGroup : CommandGroup {
if (!userResult.IsDefined(out var user)) if (!userResult.IsDefined(out var user))
return Result.FromError(userResult); return Result.FromError(userResult);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
return await AddReminderAsync(@in, message, data, channelId.Value, user, CancellationToken);
}
private async Task<Result> AddReminderAsync(
TimeSpan @in, string message, GuildData data,
Snowflake channelId, IUser user, CancellationToken ct = default) {
var remindAt = DateTimeOffset.UtcNow.Add(@in); var remindAt = DateTimeOffset.UtcNow.Add(@in);
(await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add( data.GetMemberData(user.ID).Reminders.Add(
new Reminder { new Reminder {
At = remindAt, At = remindAt,
Channel = channelId.Value.Value, Channel = channelId.Value,
Text = message Text = message
}); });
@ -71,6 +82,6 @@ public class RemindCommandGroup : CommandGroup {
.WithColour(ColorsList.Green) .WithColour(ColorsList.Green)
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -1,5 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Text; using System.Text;
using System.Text.Json.Nodes;
using Boyfriend.Data; using Boyfriend.Data;
using Boyfriend.Data.Options; using Boyfriend.Data.Options;
using Boyfriend.Services; using Boyfriend.Services;
@ -66,7 +67,7 @@ public class SettingsCommandGroup : CommandGroup {
[RequireDiscordPermission(DiscordPermission.ManageGuild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)]
[Description("Shows settings list for this server")] [Description("Shows settings list for this server")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> ListSettingsAsync() { public async Task<Result> ExecuteSettingsListAsync() {
if (!_context.TryGetContextIDs(out var guildId, out _, out _)) if (!_context.TryGetContextIDs(out var guildId, out _, out _))
return Result.FromError( return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context")); new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
@ -78,6 +79,10 @@ public class SettingsCommandGroup : CommandGroup {
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(cfg);
return await SendSettingsListAsync(cfg, currentUser, CancellationToken);
}
private async Task<Result> SendSettingsListAsync(JsonNode cfg, IUser currentUser, CancellationToken ct = default) {
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach (var option in AllOptions) { foreach (var option in AllOptions) {
@ -91,7 +96,7 @@ public class SettingsCommandGroup : CommandGroup {
.WithColour(ColorsList.Default) .WithColour(ColorsList.Default)
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
/// <summary> /// <summary>
@ -107,7 +112,7 @@ public class SettingsCommandGroup : CommandGroup {
[RequireDiscordPermission(DiscordPermission.ManageGuild)] [RequireDiscordPermission(DiscordPermission.ManageGuild)]
[Description("Change settings for this server")] [Description("Change settings for this server")]
[UsedImplicitly] [UsedImplicitly]
public async Task<Result> EditSettingsAsync( public async Task<Result> ExecuteSettingsAsync(
[Description("The setting whose value you want to change")] [Description("The setting whose value you want to change")]
string setting, string setting,
[Description("Setting value")] string value) { [Description("Setting value")] string value) {
@ -119,33 +124,38 @@ public class SettingsCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser)) if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult); return Result.FromError(currentUserResult);
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken); var data = await _dataService.GetData(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg); Messages.Culture = GuildSettings.Language.Get(data.Settings);
return await EditSettingAsync(setting, value, data, currentUser, CancellationToken);
}
private async Task<Result> EditSettingAsync(
string setting, string value, GuildData data, IUser currentUser, CancellationToken ct = default) {
var option = AllOptions.Single( var option = AllOptions.Single(
o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase)); o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase));
var setResult = option.Set(cfg, value); var setResult = option.Set(data.Settings, value);
if (!setResult.IsSuccess) { if (!setResult.IsSuccess) {
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser) var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser)
.WithDescription(setResult.Error.Message) .WithDescription(setResult.Error.Message)
.WithColour(ColorsList.Red) .WithColour(ColorsList.Red)
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
} }
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append(Markdown.InlineCode(option.Name)) builder.Append(Markdown.InlineCode(option.Name))
.Append($" {Messages.SettingIsNow} ") .Append($" {Messages.SettingIsNow} ")
.Append(option.Display(cfg)); .Append(option.Display(data.Settings));
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
.WithDescription(builder.ToString()) .WithDescription(builder.ToString())
.WithColour(ColorsList.Green) .WithColour(ColorsList.Green)
.Build(); .Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken); return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
} }
} }

View file

@ -31,6 +31,6 @@ public class InteractionResponders : InteractionGroup {
var idArray = state.Split(':'); var idArray = state.Split(':');
return (Result)await _feedbackService.SendContextualAsync( return (Result)await _feedbackService.SendContextualAsync(
$"https://discord.com/events/{idArray[0]}/{idArray[1]}", $"https://discord.com/events/{idArray[0]}/{idArray[1]}",
options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral), ct: CancellationToken);
} }
} }

View file

@ -116,63 +116,78 @@ public class GuildUpdateService : BackgroundService {
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) { private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
var data = await _dataService.GetData(guildId, ct); var data = await _dataService.GetData(guildId, ct);
Messages.Culture = GuildSettings.Language.Get(data.Settings); Messages.Culture = GuildSettings.Language.Get(data.Settings);
var defaultRole = GuildSettings.DefaultRole.Get(data.Settings); var defaultRole = GuildSettings.DefaultRole.Get(data.Settings);
foreach (var memberData in data.MemberData.Values) { foreach (var memberData in data.MemberData.Values) {
var userId = memberData.Id.ToSnowflake(); var userResult = await _userApi.GetUserAsync(memberData.Id.ToSnowflake(), ct);
if (!userResult.IsDefined(out var user)) return;
if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value)) await TickMemberAsync(guildId, user, memberData, defaultRole, ct);
_ = _guildApi.AddGuildMemberRoleAsync(
guildId, userId, defaultRole, ct: ct);
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
var unbanResult = await _guildApi.RemoveGuildBanAsync(
guildId, userId, Messages.PunishmentExpired.EncodeHeader(), ct);
if (unbanResult.IsSuccess)
memberData.BannedUntil = null;
else
_logger.LogWarning(
"Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message);
}
var userResult = await _userApi.GetUserAsync(userId, ct);
if (!userResult.IsDefined(out var user)) continue;
for (var i = memberData.Reminders.Count - 1; i >= 0; i--) {
var reminder = memberData.Reminders[i];
if (DateTimeOffset.UtcNow < reminder.At) continue;
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.Reminder, user.GetTag()), user)
.WithDescription(
string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
.WithColour(ColorsList.Magenta)
.Build();
if (!embed.IsDefined(out var built)) continue;
var messageResult = await _channelApi.CreateMessageAsync(
reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct);
if (!messageResult.IsSuccess)
_logger.LogWarning(
"Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
memberData.Reminders.Remove(reminder);
}
} }
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct); var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
if (!eventsResult.IsDefined(out var events)) return; if (!eventsResult.IsSuccess)
_logger.LogWarning("Error retrieving scheduled events.\n{ErrorMessage}", eventsResult.Error.Message);
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return; else if (!GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
await TickScheduledEventsAsync(guildId, data, eventsResult.Entity, ct);
}
private async Task TickScheduledEventsAsync(
Snowflake guildId, GuildData data, IEnumerable<IGuildScheduledEvent> events, CancellationToken ct) {
foreach (var scheduledEvent in events) { foreach (var scheduledEvent in events) {
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) { if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value))
data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status)); data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status));
} else {
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value]; var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
if (storedEvent.Status == scheduledEvent.Status) { if (storedEvent.Status == scheduledEvent.Status) {
await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct);
continue;
}
storedEvent.Status = scheduledEvent.Status;
var statusChangedResponseResult = storedEvent.Status switch {
GuildScheduledEventStatus.Scheduled =>
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct),
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
};
if (!statusChangedResponseResult.IsSuccess)
_logger.LogWarning(
"Error handling scheduled event status update.\n{ErrorMessage}",
statusChangedResponseResult.Error.Message);
}
}
private async Task TickScheduledEventAsync(
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData,
CancellationToken ct) {
if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) { if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
await TryAutoStartEventAsync(guildId, data, scheduledEvent, ct);
return;
}
if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) == TimeSpan.Zero
|| eventData.EarlyNotificationSent
|| DateTimeOffset.UtcNow
< scheduledEvent.ScheduledStartTime
- GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) return;
var earlyResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct);
if (earlyResult.IsSuccess) {
eventData.EarlyNotificationSent = true;
return;
}
_logger.LogWarning(
"Error in scheduled event early notification sender.\n{ErrorMessage}",
earlyResult.Error.Message);
}
private async Task TryAutoStartEventAsync(
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, CancellationToken ct) {
if (GuildSettings.AutoStartEvents.Get(data.Settings) if (GuildSettings.AutoStartEvents.Get(data.Settings)
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) { && scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
var startResult = await _eventApi.ModifyGuildScheduledEventAsync( var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
@ -183,37 +198,47 @@ public class GuildUpdateService : BackgroundService {
"Error in automatic scheduled event start request.\n{ErrorMessage}", "Error in automatic scheduled event start request.\n{ErrorMessage}",
startResult.Error.Message); startResult.Error.Message);
} }
} else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero }
&& !storedEvent.EarlyNotificationSent
&& DateTimeOffset.UtcNow private async Task TickMemberAsync(
>= scheduledEvent.ScheduledStartTime Snowflake guildId, IUser user, MemberData memberData, Snowflake defaultRole, CancellationToken ct) {
- GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) { if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value))
var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct); _ = _guildApi.AddGuildMemberRoleAsync(
if (earlyResult.IsSuccess) guildId, user.ID, defaultRole, ct: ct);
storedEvent.EarlyNotificationSent = true;
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
var unbanResult = await _guildApi.RemoveGuildBanAsync(
guildId, user.ID, Messages.PunishmentExpired.EncodeHeader(), ct);
if (unbanResult.IsSuccess)
memberData.BannedUntil = null;
else else
_logger.LogWarning( _logger.LogWarning(
"Error in scheduled event early notification sender.\n{ErrorMessage}", "Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message);
earlyResult.Error.Message);
} }
continue; for (var i = memberData.Reminders.Count - 1; i >= 0; i--)
await TickReminderAsync(memberData.Reminders[i], user, memberData, ct);
} }
storedEvent.Status = scheduledEvent.Status; private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) {
} if (DateTimeOffset.UtcNow < reminder.At) return;
var result = scheduledEvent.Status switch { var embed = new EmbedBuilder().WithSmallTitle(
GuildScheduledEventStatus.Scheduled => string.Format(Messages.Reminder, user.GetTag()), user)
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct), .WithDescription(
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed => string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct), .WithColour(ColorsList.Magenta)
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status))) .Build();
};
if (!result.IsSuccess) if (!embed.IsDefined(out var built)) return;
_logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message);
} var messageResult = await _channelApi.CreateMessageAsync(
reminder.Channel.ToSnowflake(), Mention.User(user), embeds: new[] { built }, ct: ct);
if (!messageResult.IsSuccess)
_logger.LogWarning(
"Error in reminder send.\n{ErrorMessage}", messageResult.Error.Message);
memberData.Reminders.Remove(reminder);
} }
/// <summary> /// <summary>
@ -228,47 +253,23 @@ public class GuildUpdateService : BackgroundService {
/// <returns>A notification sending result which may or may not have succeeded.</returns> /// <returns>A notification sending result which may or may not have succeeded.</returns>
private async Task<Result> SendScheduledEventCreatedMessage( private async Task<Result> SendScheduledEventCreatedMessage(
IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) { IGuildScheduledEvent scheduledEvent, JsonNode settings, CancellationToken ct = default) {
if (!scheduledEvent.Creator.IsDefined(out var creator))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.Creator)));
if (!scheduledEvent.CreatorID.IsDefined(out var creatorId)) Result<string> embedDescriptionResult;
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.CreatorID)));
var creatorResult = await _userApi.GetUserAsync(creatorId.Value, ct);
if (!creatorResult.IsDefined(out var creator)) return Result.FromError(creatorResult);
string embedDescription;
var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null } var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null }
? scheduledEvent.Description.Value ? scheduledEvent.Description.Value
: string.Empty; : string.Empty;
switch (scheduledEvent.EntityType) { embedDescriptionResult = scheduledEvent.EntityType switch {
case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice =>
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription),
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription(
scheduledEvent, eventDescription),
_ => Result<string>.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)))
};
embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote( if (!embedDescriptionResult.IsDefined(out var embedDescription))
string.Format( return Result.FromError(embedDescriptionResult);
Messages.DescriptionLocalEventCreated,
Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
Mention.Channel(channelId)
))}";
break;
case GuildScheduledEventEntityType.External:
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
if (!metadata.Location.IsDefined(out var location))
return Result.FromError(new ArgumentNullError(nameof(metadata.Location)));
embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote(
string.Format(
Messages.DescriptionExternalEventCreated,
Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
Markdown.Timestamp(endTime),
Markdown.InlineCode(location)
))}";
break;
default:
return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)));
}
var embed = new EmbedBuilder() var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator) .WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator)
@ -297,92 +298,153 @@ public class GuildUpdateService : BackgroundService {
components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
} }
private static Result<string> GetExternalScheduledEventCreatedEmbedDescription(
IGuildScheduledEvent scheduledEvent, string eventDescription) {
Result<string> embedDescription;
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
if (!metadata.Location.IsDefined(out var location))
return Result<string>.FromError(new ArgumentNullError(nameof(metadata.Location)));
embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote(
string.Format(
Messages.DescriptionExternalEventCreated,
Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
Markdown.Timestamp(endTime),
Markdown.InlineCode(location)
))}";
return embedDescription;
}
private static Result<string> GetLocalEventCreatedEmbedDescription(
IGuildScheduledEvent scheduledEvent, string eventDescription) {
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));
return $"{eventDescription}\n\n{Markdown.BlockQuote(
string.Format(
Messages.DescriptionLocalEventCreated,
Markdown.Timestamp(scheduledEvent.ScheduledStartTime),
Mention.Channel(channelId)
))}";
}
/// <summary> /// <summary>
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers, /// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole"/> and event subscribers,
/// when a scheduled event is about to start, has started or completed /// when a scheduled event has started or completed
/// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set. /// in a guild's <see cref="GuildSettings.EventNotificationChannel" /> if one is set.
/// </summary> /// </summary>
/// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param> /// <param name="scheduledEvent">The scheduled event that is about to start, has started or completed.</param>
/// <param name="data">The data for the guild containing the scheduled event.</param> /// <param name="data">The data for the guild containing the scheduled event.</param>
/// <param name="early">Controls whether or not a reminder for the scheduled event should be sent instead of the event started/completed notification</param>
/// <param name="ct">The cancellation token for this operation</param> /// <param name="ct">The cancellation token for this operation</param>
/// <returns>A reminder/notification sending result which may or may not have succeeded.</returns> /// <returns>A reminder/notification sending result which may or may not have succeeded.</returns>
private async Task<Result> SendScheduledEventUpdatedMessage( private async Task<Result> SendScheduledEventUpdatedMessage(
IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) { IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) {
var currentUserResult = await _userApi.GetCurrentUserAsync(ct); if (scheduledEvent.Status == GuildScheduledEventStatus.Active) {
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
var embed = new EmbedBuilder();
string? content = null;
if (early)
embed.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser)
.WithColour(ColorsList.Default);
else
switch (scheduledEvent.Status) {
case GuildScheduledEventStatus.Active:
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow; data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow;
string embedDescription; var embedDescriptionResult = scheduledEvent.EntityType switch {
switch (scheduledEvent.EntityType) { GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice =>
case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice: GetLocalEventStartedEmbedDescription(scheduledEvent),
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId)) GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent),
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID))); _ => Result<string>.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)))
};
embedDescription = string.Format(
Messages.DescriptionLocalEventStarted,
Mention.Channel(channelId)
);
break;
case GuildScheduledEventEntityType.External:
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
if (!metadata.Location.IsDefined(out var location))
return Result.FromError(new ArgumentNullError(nameof(metadata.Location)));
embedDescription = string.Format(
Messages.DescriptionExternalEventStarted,
Markdown.InlineCode(location),
Markdown.Timestamp(endTime)
);
break;
default:
return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)));
}
var contentResult = await _utility.GetEventNotificationMentions( var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data.Settings, ct); scheduledEvent, data.Settings, ct);
if (!contentResult.IsDefined(out content)) if (!contentResult.IsDefined(out var content))
return Result.FromError(contentResult); return Result.FromError(contentResult);
if (!embedDescriptionResult.IsDefined(out var embedDescription))
return Result.FromError(embedDescriptionResult);
embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name)) var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name))
.WithDescription(embedDescription) .WithDescription(embedDescription)
.WithColour(ColorsList.Green); .WithColour(ColorsList.Green)
break; .WithCurrentTimestamp()
case GuildScheduledEventStatus.Completed: .Build();
embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
if (!startedEmbed.IsDefined(out var startedBuilt)) return Result.FromError(startedEmbed);
return (Result)await _channelApi.CreateMessageAsync(
GuildSettings.EventNotificationChannel.Get(data.Settings),
content, embeds: new[] { startedBuilt }, ct: ct);
}
if (scheduledEvent.Status != GuildScheduledEventStatus.Completed)
return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)));
data.ScheduledEvents.Remove(scheduledEvent.ID.Value);
var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
.WithDescription( .WithDescription(
string.Format( string.Format(
Messages.EventDuration, Messages.EventDuration,
DateTimeOffset.UtcNow.Subtract( DateTimeOffset.UtcNow.Subtract(
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime
?? scheduledEvent.ScheduledStartTime).ToString())) ?? scheduledEvent.ScheduledStartTime).ToString()))
.WithColour(ColorsList.Black); .WithColour(ColorsList.Black)
.WithCurrentTimestamp()
.Build();
data.ScheduledEvents.Remove(scheduledEvent.ID.Value); if (!completedEmbed.IsDefined(out var completedBuilt))
break; return Result.FromError(completedEmbed);
case GuildScheduledEventStatus.Canceled:
case GuildScheduledEventStatus.Scheduled:
default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)));
}
var result = embed.WithCurrentTimestamp().Build();
if (!result.IsDefined(out var built)) return Result.FromError(result);
return (Result)await _channelApi.CreateMessageAsync( return (Result)await _channelApi.CreateMessageAsync(
GuildSettings.EventNotificationChannel.Get(data.Settings), GuildSettings.EventNotificationChannel.Get(data.Settings),
content ?? default(Optional<string>), embeds: new[] { built }, ct: ct); embeds: new[] { completedBuilt }, ct: ct);
}
private static Result<string> GetLocalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) {
Result<string> embedDescription;
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));
embedDescription = string.Format(
Messages.DescriptionLocalEventStarted,
Mention.Channel(channelId)
);
return embedDescription;
}
private static Result<string> GetExternalEventStartedEmbedDescription(IGuildScheduledEvent scheduledEvent) {
Result<string> embedDescription;
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)));
if (!scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out var endTime))
return Result<string>.FromError(new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime)));
if (!metadata.Location.IsDefined(out var location))
return Result<string>.FromError(new ArgumentNullError(nameof(metadata.Location)));
embedDescription = string.Format(
Messages.DescriptionExternalEventStarted,
Markdown.InlineCode(location),
Markdown.Timestamp(endTime)
);
return embedDescription;
}
private async Task<Result> SendEarlyEventNotificationAsync(
IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) {
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data.Settings, ct);
if (!contentResult.IsDefined(out var content))
return Result.FromError(contentResult);
var earlyResult = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.EventEarlyNotification, scheduledEvent.Name), currentUser)
.WithColour(ColorsList.Default)
.WithCurrentTimestamp()
.Build();
if (!earlyResult.IsDefined(out var earlyBuilt)) return Result.FromError(earlyResult);
return (Result)await _channelApi.CreateMessageAsync(
GuildSettings.EventNotificationChannel.Get(data.Settings),
content,
embeds: new[] { earlyBuilt }, ct: ct);
} }
} }