1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-19 16:33:36 +03:00

Merge branch 'master' into 40-rename-users-who-attempt-to-hoist-themselves

Signed-off-by: Macintosh II <95250141+mctaylors@users.noreply.github.com>
This commit is contained in:
Macintxsh 2023-07-20 23:56:51 +03:00 committed by GitHub
commit cd13be1325
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 475 additions and 427 deletions

View file

@ -12,8 +12,6 @@ updates:
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
assignees:
- "Octol1ttle"
labels:
- "type: dependencies"
@ -24,8 +22,5 @@ updates:
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
# Add assignees
assignees:
- "Octol1ttle"
labels:
- "type: dependencies"

View file

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

View file

@ -5,6 +5,7 @@ using Boyfriend.Services;
using JetBrains.Annotations;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
@ -47,7 +48,7 @@ public class AboutCommandGroup : CommandGroup {
[RequireContext(ChannelContext.Guild)]
[Description("Shows Boyfriend's developers")]
[UsedImplicitly]
public async Task<Result> SendAboutBotAsync() {
public async Task<Result> ExecuteAboutAsync() {
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
return Result.FromError(
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);
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));
foreach (var dev in Developers)
builder.AppendLine($"@{dev} — {$"AboutDeveloper@{dev}".Localized()}");
@ -73,6 +78,6 @@ public class AboutCommandGroup : CommandGroup {
.WithImageUrl("https://cdn.upload.systems/uploads/JFAaX5vr.png")
.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)]
[Description("Ban user")]
[UsedImplicitly]
public async Task<Result> ExecuteBan(
public async Task<Result> ExecuteBanAsync(
[Description("User to ban")] IUser target,
[Description("Ban reason")] string reason,
[Description("Ban duration")] TimeSpan? duration = null) {
@ -84,26 +84,26 @@ public class BanCommandGroup : CommandGroup {
if (!guildResult.IsDefined(out var guild))
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(
IUser target, string reason, TimeSpan? duration, IGuild guild, Snowflake channelId,
IUser user, IUser currentUser) {
var data = await _dataService.GetData(guild.ID, CancellationToken);
var cfg = data.Settings;
Messages.Culture = GuildSettings.Language.Get(cfg);
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, CancellationToken);
IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId,
IUser user, IUser currentUser, CancellationToken ct = default) {
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
if (existingBanResult.IsDefined()) {
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser)
.WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
}
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)
return Result.FromError(interactionResult);
@ -111,7 +111,7 @@ public class BanCommandGroup : CommandGroup {
var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.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));
@ -123,7 +123,7 @@ public class BanCommandGroup : CommandGroup {
var title = string.Format(Messages.UserBanned, target.GetTag());
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)) {
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereBanned)
@ -135,12 +135,12 @@ public class BanCommandGroup : CommandGroup {
if (!dmEmbed.IsDefined(out var dmBuilt))
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(
guild.ID, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken);
ct: ct);
if (!banResult.IsSuccess)
return Result.FromError(banResult.Error);
var memberData = data.GetMemberData(target.ID);
@ -152,11 +152,12 @@ public class BanCommandGroup : CommandGroup {
title, target)
.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)
return Result.FromError(logResult.Error);
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
/// <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
/// was unbanned and vice-versa.
/// </returns>
/// <seealso cref="ExecuteBan" />
/// <seealso cref="ExecuteBanAsync" />
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
[Command("unban")]
[DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
@ -196,25 +197,27 @@ public class BanCommandGroup : CommandGroup {
if (!userResult.IsDefined(out var user))
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(
IUser target, string reason, Snowflake guildId, Snowflake channelId, IUser user, IUser currentUser) {
var cfg = await _dataService.GetSettings(guildId, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg);
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, CancellationToken);
IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId, IUser user,
IUser currentUser, CancellationToken ct = default) {
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct);
if (!existingBanResult.IsDefined()) {
var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, currentUser)
.WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(errorEmbed, ct);
}
var unbanResult = await _guildApi.RemoveGuildBanAsync(
guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken);
ct);
if (!unbanResult.IsSuccess)
return Result.FromError(unbanResult.Error);
@ -224,10 +227,10 @@ public class BanCommandGroup : CommandGroup {
var title = string.Format(Messages.UserUnbanned, target.GetTag());
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)
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))
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(
int amount, Snowflake guildId, Snowflake channelId, IReadOnlyList<IMessage> messages,
IUser user, IUser currentUser) {
var cfg = await _dataService.GetSettings(guildId, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg);
int amount, GuildData data, Snowflake channelId, IReadOnlyList<IMessage> messages,
IUser user, IUser currentUser, CancellationToken ct = default) {
var idList = new List<Snowflake>(messages.Count);
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...')
@ -98,18 +98,18 @@ public class ClearCommandGroup : CommandGroup {
var description = builder.ToString();
var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId, idList, user.GetTag().EncodeHeader(), CancellationToken);
channelId, idList, user.GetTag().EncodeHeader(), ct);
if (!deleteResult.IsSuccess)
return Result.FromError(deleteResult.Error);
var logResult = _utility.LogActionAsync(
cfg, channelId, user, title, description, currentUser, CancellationToken);
data.Settings, channelId, user, title, description, currentUser, ct);
if (!logResult.IsSuccess)
return Result.FromError(logResult.Error);
var embed = new EmbedBuilder().WithSmallTitle(title, currentUser)
.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.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using Remora.Results;
namespace Boyfriend.Commands;
@ -62,21 +62,25 @@ public class KickCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
[Description("Kick member")]
[UsedImplicitly]
public async Task<Result> KickUserAsync(
public async Task<Result> ExecuteKick(
[Description("Member to kick")] IUser target,
[Description("Kick reason")] string reason) {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var userId))
return Result.FromError(
new ArgumentNullError(nameof(_context), "Unable to retrieve necessary IDs from command context"));
// The current user's avatar is used when sending error messages
var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!currentUserResult.IsDefined(out var currentUser))
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 cfg = data.Settings;
Messages.Culture = GuildSettings.Language.Get(cfg);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
var memberResult = await _guildApi.GetGuildMemberAsync(guildId.Value, target.ID, CancellationToken);
if (!memberResult.IsSuccess) {
@ -86,79 +90,57 @@ public class KickCommandGroup : CommandGroup {
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
= 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)
return Result.FromError(interactionResult);
Result<Embed> responseEmbed;
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();
} 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);
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)
.WithTitle(Messages.YouWereKicked)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!dmEmbed.IsDefined(out var dmBuilt))
return Result.FromError(dmEmbed);
await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: CancellationToken);
}
var kickResult = await _guildApi.RemoveGuildMemberAsync(
guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
ct: CancellationToken);
if (!kickResult.IsSuccess)
return Result.FromError(kickResult.Error);
data.GetMemberData(target.ID).Roles.Clear();
responseEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserKicked, target.GetTag()), target)
.WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
&& 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(failedEmbed, ct);
}
return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken);
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
if (dmChannelResult.IsDefined(out var dmChannel)) {
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereKicked)
.WithDescription(string.Format(Messages.DescriptionActionReason, reason))
.WithActionFooter(user)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
.Build();
if (!dmEmbed.IsDefined(out var dmBuilt))
return Result.FromError(dmEmbed);
await _channelApi.CreateMessageAsync(dmChannel.ID, embeds: new[] { dmBuilt }, ct: ct);
}
var kickResult = await _guildApi.RemoveGuildMemberAsync(
guild.ID, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
ct);
if (!kickResult.IsSuccess)
return Result.FromError(kickResult.Error);
data.GetMemberData(target.ID).Roles.Clear();
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)
.WithColour(ColorsList.Green).Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
}

View file

@ -7,13 +7,13 @@ using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
namespace Boyfriend.Commands;
@ -23,20 +23,17 @@ namespace Boyfriend.Commands;
/// </summary>
[UsedImplicitly]
public class MuteCommandGroup : CommandGroup {
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
private readonly GuildDataService _dataService;
private readonly FeedbackService _feedbackService;
private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility;
private readonly ICommandContext _context;
private readonly GuildDataService _dataService;
private readonly FeedbackService _feedbackService;
private readonly IDiscordRestGuildAPI _guildApi;
private readonly IDiscordRestUserAPI _userApi;
private readonly UtilityService _utility;
public MuteCommandGroup(
ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService,
FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
UtilityService utility) {
ICommandContext context, GuildDataService dataService, FeedbackService feedbackService,
IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, UtilityService utility) {
_context = context;
_channelApi = channelApi;
_dataService = dataService;
_feedbackService = feedbackService;
_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
/// was muted and vice-versa.
/// </returns>
/// <seealso cref="UnmuteUserAsync" />
/// <seealso cref="ExecuteUnmute" />
[Command("mute", "мут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
[DiscordDefaultDMPermission(false)]
@ -66,7 +63,7 @@ public class MuteCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Mute member")]
[UsedImplicitly]
public async Task<Result> MuteUserAsync(
public async Task<Result> ExecuteMute(
[Description("Member to mute")] IUser target,
[Description("Mute reason")] string reason,
[Description("Mute duration")] TimeSpan duration) {
@ -79,6 +76,13 @@ public class MuteCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser))
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);
if (!memberResult.IsSuccess) {
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, currentUser)
@ -87,71 +91,49 @@ public class MuteCommandGroup : CommandGroup {
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
= await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Mute", CancellationToken);
guildId, user.ID, target.ID, "Mute", ct);
if (!interactionResult.IsSuccess)
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) {
responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build();
} else {
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
var muteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: until, ct: CancellationToken);
if (!muteResult.IsSuccess)
return Result.FromError(muteResult.Error);
responseEmbed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.UserMuted, target.GetTag()), target)
.WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
&& 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(failedEmbed, ct);
}
return await _feedbackService.SendContextualEmbedResultAsync(responseEmbed, CancellationToken);
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
var muteResult = await _guildApi.ModifyGuildMemberAsync(
guildId, target.ID, reason: $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: until, ct: ct);
if (!muteResult.IsSuccess)
return Result.FromError(muteResult.Error);
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)
.WithColour(ColorsList.Green).Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
/// <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
/// was unmuted and vice-versa.
/// </returns>
/// <seealso cref="MuteUserAsync" />
/// <seealso cref="ExecuteMute" />
/// <seealso cref="GuildUpdateService.TickGuildAsync"/>
[Command("unmute", "размут")]
[DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
@ -176,7 +158,7 @@ public class MuteCommandGroup : CommandGroup {
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Unmute member")]
[UsedImplicitly]
public async Task<Result> UnmuteUserAsync(
public async Task<Result> ExecuteUnmute(
[Description("Member to unmute")] IUser target,
[Description("Unmute reason")] string reason) {
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))
return Result.FromError(currentUserResult);
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg);
// Needed to get the tag and avatar
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);
if (!memberResult.IsSuccess) {
@ -199,56 +186,43 @@ public class MuteCommandGroup : CommandGroup {
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
= await _utility.CheckInteractionsAsync(
guildId.Value, userId.Value, target.ID, "Unmute", CancellationToken);
guildId, user.ID, target.ID, "Unmute", ct);
if (!interactionResult.IsSuccess)
return Result.FromError(interactionResult);
// Needed to get the tag and avatar
var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken);
if (!userResult.IsDefined(out var user))
return Result.FromError(userResult);
if (interactionResult.Entity is not null) {
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser)
.WithColour(ColorsList.Red).Build();
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
}
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
guildId.Value, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: null, ct: CancellationToken);
guildId, target.ID, $"({user.GetTag()}) {reason}".EncodeHeader(),
communicationDisabledUntil: null, ct: ct);
if (!unmuteResult.IsSuccess)
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)
.WithColour(ColorsList.Green).Build();
if ((!GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
&& 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);
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
}

View file

@ -4,6 +4,7 @@ using Boyfriend.Services;
using JetBrains.Annotations;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway;
using Remora.Rest.Core;
using Remora.Results;
namespace Boyfriend.Commands;
@ -49,7 +51,7 @@ public class PingCommandGroup : CommandGroup {
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
[UsedImplicitly]
public async Task<Result> SendPingAsync() {
public async Task<Result> ExecutePingAsync() {
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _))
return Result.FromError(
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);
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;
if (latency is 0) {
// No heartbeat has occurred, estimate latency from local time and "Boyfriend is thinking..." message
var lastMessageResult = await _channelApi.GetChannelMessagesAsync(
channelId.Value, limit: 1, ct: CancellationToken);
channelId, limit: 1, ct: ct);
if (!lastMessageResult.IsDefined(out var lastMessage))
return Result.FromError(lastMessageResult);
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
@ -78,6 +85,6 @@ public class PingCommandGroup : CommandGroup {
.WithCurrentTimestamp()
.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 Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
@ -11,6 +12,7 @@ using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
namespace Boyfriend.Commands;
@ -45,7 +47,7 @@ public class RemindCommandGroup : CommandGroup {
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
[UsedImplicitly]
public async Task<Result> AddReminderAsync(
public async Task<Result> ExecuteReminderAsync(
[Description("After what period of time mention the reminder")]
TimeSpan @in,
[Description("Reminder message")] string message) {
@ -57,12 +59,21 @@ public class RemindCommandGroup : CommandGroup {
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);
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);
(await _dataService.GetMemberData(guildId.Value, userId.Value, CancellationToken)).Reminders.Add(
data.GetMemberData(user.ID).Reminders.Add(
new Reminder {
At = remindAt,
Channel = channelId.Value.Value,
Channel = channelId.Value,
Text = message
});
@ -71,6 +82,6 @@ public class RemindCommandGroup : CommandGroup {
.WithColour(ColorsList.Green)
.Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
}

View file

@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Text;
using System.Text.Json.Nodes;
using Boyfriend.Data;
using Boyfriend.Data.Options;
using Boyfriend.Services;
@ -66,7 +67,7 @@ public class SettingsCommandGroup : CommandGroup {
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
[Description("Shows settings list for this server")]
[UsedImplicitly]
public async Task<Result> ListSettingsAsync() {
public async Task<Result> ExecuteSettingsListAsync() {
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
return Result.FromError(
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);
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();
foreach (var option in AllOptions) {
@ -91,7 +96,7 @@ public class SettingsCommandGroup : CommandGroup {
.WithColour(ColorsList.Default)
.Build();
return await _feedbackService.SendContextualEmbedResultAsync(embed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(embed, ct);
}
/// <summary>
@ -107,7 +112,7 @@ public class SettingsCommandGroup : CommandGroup {
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
[Description("Change settings for this server")]
[UsedImplicitly]
public async Task<Result> EditSettingsAsync(
public async Task<Result> ExecuteSettingsAsync(
[Description("The setting whose value you want to change")]
string setting,
[Description("Setting value")] string value) {
@ -119,33 +124,38 @@ public class SettingsCommandGroup : CommandGroup {
if (!currentUserResult.IsDefined(out var currentUser))
return Result.FromError(currentUserResult);
var cfg = await _dataService.GetSettings(guildId.Value, CancellationToken);
Messages.Culture = GuildSettings.Language.Get(cfg);
var data = await _dataService.GetData(guildId.Value, CancellationToken);
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(
o => string.Equals(setting, o.Name, StringComparison.InvariantCultureIgnoreCase));
var setResult = option.Set(cfg, value);
var setResult = option.Set(data.Settings, value);
if (!setResult.IsSuccess) {
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, currentUser)
.WithDescription(setResult.Error.Message)
.WithColour(ColorsList.Red)
.Build();
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, CancellationToken);
return await _feedbackService.SendContextualEmbedResultAsync(failedEmbed, ct);
}
var builder = new StringBuilder();
builder.Append(Markdown.InlineCode(option.Name))
.Append($" {Messages.SettingIsNow} ")
.Append(option.Display(cfg));
.Append(option.Display(data.Settings));
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser)
.WithDescription(builder.ToString())
.WithColour(ColorsList.Green)
.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(':');
return (Result)await _feedbackService.SendContextualAsync(
$"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,49 +116,12 @@ public class GuildUpdateService : BackgroundService {
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
var data = await _dataService.GetData(guildId, ct);
Messages.Culture = GuildSettings.Language.Get(data.Settings);
var defaultRole = GuildSettings.DefaultRole.Get(data.Settings);
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))
_ = _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 guildUser = _guildApi.GetGuildMemberAsync(guildId, userId, ct);
var tag = guildUser.Result.Entity.Nickname.ToString();
@ -169,65 +132,125 @@ public class GuildUpdateService : BackgroundService {
await _guildApi.ModifyGuildMemberAsync(guildId, userId, "nickname", ct: ct);
}
}
await TickMemberAsync(guildId, user, memberData, defaultRole, ct);
}
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
if (!eventsResult.IsDefined(out var events)) return;
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) return;
if (!eventsResult.IsSuccess)
_logger.LogWarning("Error retrieving scheduled events.\n{ErrorMessage}", eventsResult.Error.Message);
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) {
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value)) {
if (!data.ScheduledEvents.ContainsKey(scheduledEvent.ID.Value))
data.ScheduledEvents.Add(scheduledEvent.ID.Value, new ScheduledEventData(scheduledEvent.Status));
} else {
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
if (storedEvent.Status == scheduledEvent.Status) {
if (DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime) {
if (GuildSettings.AutoStartEvents.Get(data.Settings)
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
guildId, scheduledEvent.ID,
status: GuildScheduledEventStatus.Active, ct: ct);
if (!startResult.IsSuccess)
_logger.LogWarning(
"Error in automatic scheduled event start request.\n{ErrorMessage}",
startResult.Error.Message);
}
} else if (GuildSettings.EventEarlyNotificationOffset.Get(data.Settings) != TimeSpan.Zero
&& !storedEvent.EarlyNotificationSent
&& DateTimeOffset.UtcNow
>= scheduledEvent.ScheduledStartTime
- GuildSettings.EventEarlyNotificationOffset.Get(data.Settings)) {
var earlyResult = await SendScheduledEventUpdatedMessage(scheduledEvent, data, true, ct);
if (earlyResult.IsSuccess)
storedEvent.EarlyNotificationSent = true;
else
_logger.LogWarning(
"Error in scheduled event early notification sender.\n{ErrorMessage}",
earlyResult.Error.Message);
}
continue;
}
storedEvent.Status = scheduledEvent.Status;
var storedEvent = data.ScheduledEvents[scheduledEvent.ID.Value];
if (storedEvent.Status == scheduledEvent.Status) {
await TickScheduledEventAsync(guildId, data, scheduledEvent, storedEvent, ct);
continue;
}
storedEvent.Status = scheduledEvent.Status;
var result = scheduledEvent.Status switch {
var statusChangedResponseResult = storedEvent.Status switch {
GuildScheduledEventStatus.Scheduled =>
await SendScheduledEventCreatedMessage(scheduledEvent, data.Settings, ct),
GuildScheduledEventStatus.Active or GuildScheduledEventStatus.Completed =>
await SendScheduledEventUpdatedMessage(scheduledEvent, data, false, ct),
await SendScheduledEventUpdatedMessage(scheduledEvent, data, ct),
_ => Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)))
};
if (!result.IsSuccess)
_logger.LogWarning("Error in guild update.\n{ErrorMessage}", result.Error.Message);
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) {
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)
&& scheduledEvent.Status is not GuildScheduledEventStatus.Active) {
var startResult = await _eventApi.ModifyGuildScheduledEventAsync(
guildId, scheduledEvent.ID,
status: GuildScheduledEventStatus.Active, ct: ct);
if (!startResult.IsSuccess)
_logger.LogWarning(
"Error in automatic scheduled event start request.\n{ErrorMessage}",
startResult.Error.Message);
}
}
private async Task TickMemberAsync(
Snowflake guildId, IUser user, MemberData memberData, Snowflake defaultRole, CancellationToken ct) {
if (defaultRole.Value is not 0 && !memberData.Roles.Contains(defaultRole.Value))
_ = _guildApi.AddGuildMemberRoleAsync(
guildId, user.ID, defaultRole, ct: ct);
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
var unbanResult = await _guildApi.RemoveGuildBanAsync(
guildId, user.ID, Messages.PunishmentExpired.EncodeHeader(), ct);
if (unbanResult.IsSuccess)
memberData.BannedUntil = null;
else
_logger.LogWarning(
"Error in automatic user unban request.\n{ErrorMessage}", unbanResult.Error.Message);
}
for (var i = memberData.Reminders.Count - 1; i >= 0; i--)
await TickReminderAsync(memberData.Reminders[i], user, memberData, ct);
}
private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData memberData, CancellationToken ct) {
if (DateTimeOffset.UtcNow < reminder.At) return;
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)) return;
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>
/// Handles sending a notification, mentioning the <see cref="GuildSettings.EventNotificationRole" /> if one is
/// set,
@ -240,47 +263,23 @@ public class GuildUpdateService : BackgroundService {
/// <returns>A notification sending result which may or may not have succeeded.</returns>
private async Task<Result> SendScheduledEventCreatedMessage(
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))
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;
Result<string> embedDescriptionResult;
var eventDescription = scheduledEvent.Description is { HasValue: true, Value: not null }
? scheduledEvent.Description.Value
: string.Empty;
switch (scheduledEvent.EntityType) {
case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice:
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));
embedDescriptionResult = scheduledEvent.EntityType switch {
GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice =>
GetLocalEventCreatedEmbedDescription(scheduledEvent, eventDescription),
GuildScheduledEventEntityType.External => GetExternalScheduledEventCreatedEmbedDescription(
scheduledEvent, eventDescription),
_ => Result<string>.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)))
};
embedDescription = $"{eventDescription}\n\n{Markdown.BlockQuote(
string.Format(
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)));
}
if (!embedDescriptionResult.IsDefined(out var embedDescription))
return Result.FromError(embedDescriptionResult);
var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.EventCreatedTitle, creator.GetTag()), creator)
@ -309,92 +308,153 @@ public class GuildUpdateService : BackgroundService {
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>
/// 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.
/// </summary>
/// <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="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>
/// <returns>A reminder/notification sending result which may or may not have succeeded.</returns>
private async Task<Result> SendScheduledEventUpdatedMessage(
IGuildScheduledEvent scheduledEvent, GuildData data, bool early, CancellationToken ct = default) {
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) {
if (scheduledEvent.Status == GuildScheduledEventStatus.Active) {
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime = DateTimeOffset.UtcNow;
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;
var embedDescriptionResult = scheduledEvent.EntityType switch {
GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice =>
GetLocalEventStartedEmbedDescription(scheduledEvent),
GuildScheduledEventEntityType.External => GetExternalEventStartedEmbedDescription(scheduledEvent),
_ => Result<string>.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)))
};
string embedDescription;
switch (scheduledEvent.EntityType) {
case GuildScheduledEventEntityType.StageInstance or GuildScheduledEventEntityType.Voice:
if (!scheduledEvent.ChannelID.AsOptional().IsDefined(out var channelId))
return Result.FromError(new ArgumentNullError(nameof(scheduledEvent.ChannelID)));
var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data.Settings, ct);
if (!contentResult.IsDefined(out var content))
return Result.FromError(contentResult);
if (!embedDescriptionResult.IsDefined(out var embedDescription))
return Result.FromError(embedDescriptionResult);
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)));
var startedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name))
.WithDescription(embedDescription)
.WithColour(ColorsList.Green)
.WithCurrentTimestamp()
.Build();
embedDescription = string.Format(
Messages.DescriptionExternalEventStarted,
Markdown.InlineCode(location),
Markdown.Timestamp(endTime)
);
break;
default:
return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.EntityType)));
}
if (!startedEmbed.IsDefined(out var startedBuilt)) return Result.FromError(startedEmbed);
var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data.Settings, ct);
if (!contentResult.IsDefined(out content))
return Result.FromError(contentResult);
return (Result)await _channelApi.CreateMessageAsync(
GuildSettings.EventNotificationChannel.Get(data.Settings),
content, embeds: new[] { startedBuilt }, ct: ct);
}
embed.WithTitle(string.Format(Messages.EventStarted, scheduledEvent.Name))
.WithDescription(embedDescription)
.WithColour(ColorsList.Green);
break;
case GuildScheduledEventStatus.Completed:
embed.WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
.WithDescription(
string.Format(
Messages.EventDuration,
DateTimeOffset.UtcNow.Subtract(
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime
?? scheduledEvent.ScheduledStartTime).ToString()))
.WithColour(ColorsList.Black);
if (scheduledEvent.Status != GuildScheduledEventStatus.Completed)
return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)));
data.ScheduledEvents.Remove(scheduledEvent.ID.Value);
data.ScheduledEvents.Remove(scheduledEvent.ID.Value);
break;
case GuildScheduledEventStatus.Canceled:
case GuildScheduledEventStatus.Scheduled:
default: return Result.FromError(new ArgumentOutOfRangeError(nameof(scheduledEvent.Status)));
}
var completedEmbed = new EmbedBuilder().WithTitle(string.Format(Messages.EventCompleted, scheduledEvent.Name))
.WithDescription(
string.Format(
Messages.EventDuration,
DateTimeOffset.UtcNow.Subtract(
data.ScheduledEvents[scheduledEvent.ID.Value].ActualStartTime
?? scheduledEvent.ScheduledStartTime).ToString()))
.WithColour(ColorsList.Black)
.WithCurrentTimestamp()
.Build();
var result = embed.WithCurrentTimestamp().Build();
if (!result.IsDefined(out var built)) return Result.FromError(result);
if (!completedEmbed.IsDefined(out var completedBuilt))
return Result.FromError(completedEmbed);
return (Result)await _channelApi.CreateMessageAsync(
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);
}
}