mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-05-01 19:49:55 +03:00
Apply official naming guidelines to Octobot (#306)
1. The root namespace was changed from `Octobot` to `TeamOctolings.Octobot`: > DO prefix namespace names with a company name to prevent namespaces from different companies from having the same name. 2. `Octobot.cs` was renamed to `Program.cs`: > DO NOT use the same name for a namespace and a type in that namespace. 3. `IOption`, `Option` were renamed to `IGuildOption` and `GuildOption` respectively: > DO NOT introduce generic type names such as Element, Node, Log, and Message. 4. `Utility` was moved out of the `Services` namespace. It didn't belong there anyway 5. `Program` static fields were moved to `Utility` 6. Localisation files were moved back to the project source files. Looks like this fixed `Message.Designer.cs` code generation --------- Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
19fadead91
commit
793afd0e06
61 changed files with 447 additions and 462 deletions
137
TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
Normal file
137
TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
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.API.Objects;
|
||||
using Remora.Discord.Commands.Attributes;
|
||||
using Remora.Discord.Commands.Conditions;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Messages;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the command to show information about this bot: /about.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class AboutCommandGroup : CommandGroup
|
||||
{
|
||||
private static readonly (string Username, Snowflake Id)[] Developers =
|
||||
[
|
||||
("Octol1ttle", new Snowflake(504343489664909322)),
|
||||
("mctaylors", new Snowflake(326642240229474304)),
|
||||
("neroduckale", new Snowflake(474943797063843851))
|
||||
];
|
||||
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
|
||||
public AboutCommandGroup(
|
||||
ICommandContext context, GuildDataService guildData,
|
||||
IFeedbackService feedback, IDiscordRestUserAPI userApi,
|
||||
IDiscordRestGuildAPI guildApi)
|
||||
{
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
_guildApi = guildApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows information about this bot.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("about")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[Description("Shows Octobot's developers")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteAboutAsync()
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
return await SendAboutBotAsync(bot, guildId, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> SendAboutBotAsync(IUser bot, Snowflake guildId, CancellationToken ct = default)
|
||||
{
|
||||
var builder = new StringBuilder().Append("### ").AppendLine(Messages.AboutTitleDevelopers);
|
||||
foreach (var dev in Developers)
|
||||
{
|
||||
var guildMemberResult = await _guildApi.GetGuildMemberAsync(
|
||||
guildId, dev.Id, ct);
|
||||
var tag = guildMemberResult.IsSuccess
|
||||
? $"<@{dev.Id}>"
|
||||
: Markdown.Hyperlink($"@{dev.Username}", $"https://github.com/{dev.Username}");
|
||||
|
||||
builder.AppendBulletPointLine($"{tag} — {$"AboutDeveloper@{dev.Username}".Localized()}");
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/HEAD/docs/octobot-banner.png")
|
||||
.WithFooter(string.Format(Messages.Version, BuildInfo.Version))
|
||||
.Build();
|
||||
|
||||
var repositoryButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonOpenRepository,
|
||||
new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310)
|
||||
URL: BuildInfo.RepositoryUrl
|
||||
);
|
||||
|
||||
var wikiButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
Messages.ButtonOpenWiki,
|
||||
new PartialEmoji(Name: "\ud83d\udcd6"), // 'OPEN BOOK' (U+1F4D6)
|
||||
URL: BuildInfo.WikiUrl
|
||||
);
|
||||
|
||||
var issuesButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
BuildInfo.IsDirty
|
||||
? Messages.ButtonDirty
|
||||
: Messages.ButtonReportIssue,
|
||||
new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0)
|
||||
URL: BuildInfo.IssuesUrl,
|
||||
IsDisabled: BuildInfo.IsDirty
|
||||
);
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed,
|
||||
new FeedbackMessageOptions(MessageComponents: new[]
|
||||
{
|
||||
new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton })
|
||||
}), ct);
|
||||
}
|
||||
}
|
300
TeamOctolings.Octobot/Commands/BanCommandGroup.cs
Normal file
300
TeamOctolings.Octobot/Commands/BanCommandGroup.cs
Normal file
|
@ -0,0 +1,300 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Parsers;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
using TeamOctolings.Octobot.Services.Update;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles commands related to ban management: /ban and /unban.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class BanCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly AccessControlService _access;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
|
||||
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
|
||||
IDiscordRestUserAPI userApi, Utility utility)
|
||||
{
|
||||
_access = access;
|
||||
_channelApi = channelApi;
|
||||
_context = context;
|
||||
_feedback = feedback;
|
||||
_guildApi = guildApi;
|
||||
_guildData = guildData;
|
||||
_userApi = userApi;
|
||||
_utility = utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that bans a Discord user with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The user to ban.</param>
|
||||
/// <param name="duration">The duration for this ban. The user will be automatically unbanned after this duration.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this ban. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.CreateGuildBanAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
|
||||
/// was banned and vice-versa.
|
||||
/// </returns>
|
||||
/// <seealso cref="ExecuteUnban" />
|
||||
[Command("ban", "бан")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||
[Description("Ban user")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteBanAsync(
|
||||
[Description("User to ban")] IUser target,
|
||||
[Description("Ban reason")] [MaxLength(256)]
|
||||
string reason,
|
||||
[Description("Ban duration (e.g. 1h30m)")]
|
||||
string? duration = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending error messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guild.ID, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
if (duration is null)
|
||||
{
|
||||
return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot,
|
||||
CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(duration);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> BanUserAsync(
|
||||
IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data,
|
||||
Snowflake channelId,
|
||||
IUser bot, CancellationToken ct = default)
|
||||
{
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
|
||||
if (existingBanResult.IsDefined())
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var interactionResult
|
||||
= await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct);
|
||||
if (!interactionResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
{
|
||||
var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var builder =
|
||||
new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
|
||||
if (duration is not null)
|
||||
{
|
||||
builder.AppendBulletPoint(
|
||||
string.Format(
|
||||
Messages.DescriptionActionExpiresAt,
|
||||
Markdown.Timestamp(DateTimeOffset.UtcNow.Add(duration.Value))));
|
||||
}
|
||||
|
||||
var title = string.Format(Messages.UserBanned, target.GetTag());
|
||||
var description = builder.ToString();
|
||||
|
||||
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
|
||||
if (dmChannelResult.IsDefined(out var dmChannel))
|
||||
{
|
||||
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
|
||||
.WithTitle(Messages.YouWereBanned)
|
||||
.WithDescription(description)
|
||||
.WithActionFooter(executor)
|
||||
.WithCurrentTimestamp()
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.BannedUntil
|
||||
= duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
|
||||
|
||||
var banResult = await _guildApi.CreateGuildBanAsync(
|
||||
guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
ct: ct);
|
||||
if (!banResult.IsSuccess)
|
||||
{
|
||||
memberData.BannedUntil = null;
|
||||
return ResultExtensions.FromError(banResult);
|
||||
}
|
||||
|
||||
memberData.Roles.Clear();
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
title, target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that unbans a Discord user with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The user to unban.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this unban. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.RemoveGuildBanAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 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="ExecuteBanAsync" />
|
||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||
[Command("unban")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||
[Description("Unban user")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteUnban(
|
||||
[Description("User to unban")] IUser target,
|
||||
[Description("Unban reason")] [MaxLength(256)]
|
||||
string reason)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending error messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
// Needed to get the tag and avatar
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await UnbanUserAsync(executor, target, reason, guildId, data, channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> UnbanUserAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId,
|
||||
IUser bot, CancellationToken ct = default)
|
||||
{
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct);
|
||||
if (!existingBanResult.IsDefined())
|
||||
{
|
||||
var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotBanned, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
||||
guildId, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
ct);
|
||||
if (!unbanResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(unbanResult);
|
||||
}
|
||||
|
||||
data.GetOrCreateMemberData(target.ID).BannedUntil = null;
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserUnbanned, target.GetTag()), target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
var title = string.Format(Messages.UserUnbanned, target.GetTag());
|
||||
var description =
|
||||
new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
169
TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
Normal file
169
TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
Normal file
|
@ -0,0 +1,169 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the command to clear messages in a channel: /clear.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ClearCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public ClearCommandGroup(
|
||||
IDiscordRestChannelAPI channelApi, ICommandContext context, GuildDataService guildData,
|
||||
IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
|
||||
{
|
||||
_channelApi = channelApi;
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
_utility = utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that clears messages in the channel it was executed, optionally filtering by message author.
|
||||
/// </summary>
|
||||
/// <param name="amount">The amount of messages to clear.</param>
|
||||
/// <param name="author">The user whose messages will be cleared.</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages
|
||||
/// were cleared and vice-versa.
|
||||
/// </returns>
|
||||
[Command("clear", "очистить")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.ManageMessages)]
|
||||
[Description("Remove multiple messages")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteClear(
|
||||
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
|
||||
int amount,
|
||||
IUser? author = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var messagesResult = await _channelApi.GetChannelMessagesAsync(
|
||||
channelId, limit: amount + 1, ct: CancellationToken);
|
||||
if (!messagesResult.IsDefined(out var messages))
|
||||
{
|
||||
return ResultExtensions.FromError(messagesResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await ClearMessagesAsync(executor, author, data, channelId, messages, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> ClearMessagesAsync(
|
||||
IUser executor, IUser? author, GuildData data, Snowflake channelId, IReadOnlyList<IMessage> messages, IUser bot,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var idList = new List<Snowflake>(messages.Count);
|
||||
|
||||
var logEntries = new List<ClearedMessageEntry> { new() };
|
||||
var currentLogEntry = 0;
|
||||
for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...')
|
||||
{
|
||||
var message = messages[i];
|
||||
if (author is not null && message.Author.ID != author.ID)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
idList.Add(message.ID);
|
||||
|
||||
var entry = logEntries[currentLogEntry];
|
||||
var str = $"{string.Format(Messages.MessageFrom, Mention.User(message.Author))}\n{message.Content.InBlockCode()}";
|
||||
if (entry.Builder.Length + str.Length > EmbedConstants.MaxDescriptionLength)
|
||||
{
|
||||
logEntries.Add(entry = new ClearedMessageEntry());
|
||||
currentLogEntry++;
|
||||
}
|
||||
|
||||
entry.Builder.Append(str);
|
||||
entry.DeletedCount++;
|
||||
}
|
||||
|
||||
if (idList.Count == 0)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoMessagesToClear, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var title = author is not null
|
||||
? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag())
|
||||
: string.Format(Messages.MessagesCleared, idList.Count.ToString());
|
||||
|
||||
var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
|
||||
channelId, idList, executor.GetTag().EncodeHeader(), ct);
|
||||
if (!deleteResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(deleteResult);
|
||||
}
|
||||
|
||||
foreach (var log in logEntries)
|
||||
{
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, author is not null
|
||||
? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag())
|
||||
: string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()),
|
||||
log.Builder.ToString(), bot, ColorsList.Red, false, ct);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private sealed class ClearedMessageEntry
|
||||
{
|
||||
public StringBuilder Builder { get; } = new();
|
||||
public int DeletedCount { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Messages;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Commands.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Handles error logging for slash command groups.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
|
||||
{
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly ILogger<ErrorLoggingPostExecutionEvent> _logger;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public ErrorLoggingPostExecutionEvent(ILogger<ErrorLoggingPostExecutionEvent> logger, IFeedbackService feedback,
|
||||
IDiscordRestUserAPI userApi)
|
||||
{
|
||||
_logger = logger;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning using the injected <see cref="ILogger" /> if the <paramref name="commandResult" /> has not
|
||||
/// succeeded.
|
||||
/// </summary>
|
||||
/// <param name="context">The context of the slash command.</param>
|
||||
/// <param name="commandResult">The result whose success is checked.</param>
|
||||
/// <param name="ct">The cancellation token for this operation. Unused.</param>
|
||||
/// <returns>A result which has succeeded.</returns>
|
||||
public async Task<Result> AfterExecutionAsync(
|
||||
ICommandContext context, IResult commandResult, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogResult(commandResult, $"Error in slash command execution for /{context.Command.Command.Node.Key}.");
|
||||
|
||||
var result = commandResult;
|
||||
while (result.Inner is not null)
|
||||
{
|
||||
result = result.Inner;
|
||||
}
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot)
|
||||
.WithDescription(Markdown.InlineCode(result.Error.Message))
|
||||
.WithFooter(Messages.ContactDevelopers)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
var issuesButton = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
BuildInfo.IsDirty
|
||||
? Messages.ButtonDirty
|
||||
: Messages.ButtonReportIssue,
|
||||
new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0)
|
||||
URL: BuildInfo.IssuesUrl,
|
||||
IsDisabled: BuildInfo.IsDirty
|
||||
);
|
||||
|
||||
return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed,
|
||||
new FeedbackMessageOptions(MessageComponents: new[]
|
||||
{
|
||||
new ActionRowComponent(new[] { issuesButton })
|
||||
}), ct)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Services;
|
||||
using Remora.Results;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Handles error logging for slash commands that couldn't be successfully prepared.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class LoggingPreparationErrorEvent : IPreparationErrorEvent
|
||||
{
|
||||
private readonly ILogger<LoggingPreparationErrorEvent> _logger;
|
||||
|
||||
public LoggingPreparationErrorEvent(ILogger<LoggingPreparationErrorEvent> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning using the injected <see cref="ILogger" /> if the <paramref name="preparationResult" /> has not
|
||||
/// succeeded.
|
||||
/// </summary>
|
||||
/// <param name="context">The context of the slash command. Unused.</param>
|
||||
/// <param name="preparationResult">The result whose success is checked.</param>
|
||||
/// <param name="ct">The cancellation token for this operation. Unused.</param>
|
||||
/// <returns>A result which has succeeded.</returns>
|
||||
public Task<Result> PreparationFailed(
|
||||
IOperationContext context, IResult preparationResult, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogResult(preparationResult, "Error in slash command preparation.");
|
||||
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
}
|
174
TeamOctolings.Octobot/Commands/KickCommandGroup.cs
Normal file
174
TeamOctolings.Octobot/Commands/KickCommandGroup.cs
Normal file
|
@ -0,0 +1,174 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the command to kick members of a guild: /kick.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class KickCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly AccessControlService _access;
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
|
||||
IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
|
||||
IDiscordRestUserAPI userApi, Utility utility)
|
||||
{
|
||||
_access = access;
|
||||
_channelApi = channelApi;
|
||||
_context = context;
|
||||
_feedback = feedback;
|
||||
_guildApi = guildApi;
|
||||
_guildData = guildData;
|
||||
_userApi = userApi;
|
||||
_utility = utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that kicks a Discord member with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The member to kick.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this kick. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.RemoveGuildMemberAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
|
||||
/// was kicked and vice-versa.
|
||||
/// </returns>
|
||||
[Command("kick", "кик")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
|
||||
[Description("Kick member")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteKick(
|
||||
[Description("Member to kick")] IUser target,
|
||||
[Description("Kick reason")] [MaxLength(256)]
|
||||
string reason)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending error messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken);
|
||||
if (!memberResult.IsSuccess)
|
||||
{
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> KickUserAsync(
|
||||
IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var interactionResult
|
||||
= await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct);
|
||||
if (!interactionResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
|
||||
if (dmChannelResult.IsDefined(out var dmChannel))
|
||||
{
|
||||
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
|
||||
.WithTitle(Messages.YouWereKicked)
|
||||
.WithDescription(
|
||||
MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
|
||||
.WithActionFooter(executor)
|
||||
.WithCurrentTimestamp()
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
memberData.Kicked = true;
|
||||
|
||||
var kickResult = await _guildApi.RemoveGuildMemberAsync(
|
||||
guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
ct);
|
||||
if (!kickResult.IsSuccess)
|
||||
{
|
||||
memberData.Kicked = false;
|
||||
return ResultExtensions.FromError(kickResult);
|
||||
}
|
||||
|
||||
memberData.Roles.Clear();
|
||||
|
||||
var title = string.Format(Messages.UserKicked, target.GetTag());
|
||||
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserKicked, target.GetTag()), target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
388
TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
Normal file
388
TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
Normal file
|
@ -0,0 +1,388 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Parsers;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
using TeamOctolings.Octobot.Services.Update;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles commands related to mute management: /mute and /unmute.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class MuteCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly AccessControlService _access;
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback,
|
||||
IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility)
|
||||
{
|
||||
_access = access;
|
||||
_context = context;
|
||||
_feedback = feedback;
|
||||
_guildApi = guildApi;
|
||||
_guildData = guildData;
|
||||
_userApi = userApi;
|
||||
_utility = utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that mutes a Discord member with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The member to mute.</param>
|
||||
/// <param name="stringDuration">The duration for this mute. The member will be automatically unmuted after this duration.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this mute. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.ModifyGuildMemberAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 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="ExecuteUnmute" />
|
||||
[Command("mute", "мут")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||
[Description("Mute member")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteMute(
|
||||
[Description("Member to mute")] IUser target,
|
||||
[Description("Mute reason")] [MaxLength(256)]
|
||||
string reason,
|
||||
[Description("Mute duration (e.g. 1h30m)")] [Option("duration")]
|
||||
string stringDuration)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending error messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken);
|
||||
if (!memberResult.IsSuccess)
|
||||
{
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(stringDuration);
|
||||
if (!parseResult.IsDefined(out var duration))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot,
|
||||
CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> MuteUserAsync(
|
||||
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data,
|
||||
Snowflake channelId, IUser bot, CancellationToken ct = default)
|
||||
{
|
||||
var interactionResult
|
||||
= await _access.CheckInteractionsAsync(
|
||||
guildId, executor.ID, target.ID, "Mute", ct);
|
||||
if (!interactionResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
|
||||
|
||||
var muteMethodResult =
|
||||
await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct);
|
||||
if (!muteMethodResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(muteMethodResult);
|
||||
}
|
||||
|
||||
var title = string.Format(Messages.UserMuted, target.GetTag());
|
||||
var description = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
|
||||
.AppendBulletPoint(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserMuted, target.GetTag()), target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private async Task<Result> SelectMuteMethodAsync(
|
||||
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data,
|
||||
IUser bot, DateTimeOffset until, CancellationToken ct)
|
||||
{
|
||||
var muteRole = GuildSettings.MuteRole.Get(data.Settings);
|
||||
|
||||
if (muteRole.Empty())
|
||||
{
|
||||
var timeoutResult = await TimeoutUserAsync(executor, target, reason, duration, guildId, bot, until, ct);
|
||||
return timeoutResult;
|
||||
}
|
||||
|
||||
var muteRoleResult = await RoleMuteUserAsync(executor, target, reason, guildId, data, until, muteRole, ct);
|
||||
return muteRoleResult;
|
||||
}
|
||||
|
||||
private async Task<Result> RoleMuteUserAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, GuildData data,
|
||||
DateTimeOffset until, Snowflake muteRole, CancellationToken ct)
|
||||
{
|
||||
var assignRoles = new List<Snowflake> { muteRole };
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
if (!GuildSettings.RemoveRolesOnMute.Get(data.Settings))
|
||||
{
|
||||
assignRoles.AddRange(memberData.Roles.ConvertAll(r => r.ToSnowflake()));
|
||||
}
|
||||
|
||||
var muteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
guildId, target.ID, roles: assignRoles,
|
||||
reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct);
|
||||
if (muteResult.IsSuccess)
|
||||
{
|
||||
memberData.MutedUntil = until;
|
||||
}
|
||||
|
||||
return muteResult;
|
||||
}
|
||||
|
||||
private async Task<Result> TimeoutUserAsync(
|
||||
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId,
|
||||
IUser bot, DateTimeOffset until, CancellationToken ct)
|
||||
{
|
||||
if (duration.TotalDays >= 28)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.BotCannotMuteTarget, bot)
|
||||
.WithDescription(Messages.DurationRequiredForTimeOuts)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var muteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
guildId, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
communicationDisabledUntil: until, ct: ct);
|
||||
return muteResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that unmutes a Discord member with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The member to unmute.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this unmute. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.ModifyGuildMemberAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 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="ExecuteMute" />
|
||||
/// <seealso cref="MemberUpdateService.TickMemberDataAsync" />
|
||||
[Command("unmute", "размут")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
|
||||
[Description("Unmute member")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteUnmute(
|
||||
[Description("Member to unmute")] IUser target,
|
||||
[Description("Unmute reason")] [MaxLength(256)]
|
||||
string reason)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
// The bot's avatar is used when sending error messages
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
// Needed to get the tag and avatar
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var memberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, CancellationToken);
|
||||
if (!memberResult.IsSuccess)
|
||||
{
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.UserNotFoundShort, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await RemoveMuteAsync(executor, target, reason, guildId, data, channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> RemoveMuteAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, Snowflake channelId,
|
||||
IUser bot, CancellationToken ct = default)
|
||||
{
|
||||
var interactionResult
|
||||
= await _access.CheckInteractionsAsync(
|
||||
guildId, executor.ID, target.ID, "Unmute", ct);
|
||||
if (!interactionResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(interactionResult);
|
||||
}
|
||||
|
||||
if (interactionResult.Entity is not null)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct);
|
||||
DateTimeOffset? communicationDisabledUntil = null;
|
||||
if (guildMemberResult.IsDefined(out var guildMember))
|
||||
{
|
||||
communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null);
|
||||
}
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
var wasMuted = memberData.MutedUntil is not null || communicationDisabledUntil is not null;
|
||||
|
||||
if (!wasMuted)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserNotMuted, bot)
|
||||
.WithColour(ColorsList.Red).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var removeMuteRoleAsync =
|
||||
await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken);
|
||||
if (!removeMuteRoleAsync.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(removeMuteRoleAsync);
|
||||
}
|
||||
|
||||
var removeTimeoutResult =
|
||||
await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken);
|
||||
if (!removeTimeoutResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(removeTimeoutResult);
|
||||
}
|
||||
|
||||
var title = string.Format(Messages.UserUnmuted, target.GetTag());
|
||||
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private async Task<Result> RemoveMuteRoleAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (memberData.MutedUntil is null)
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
guildId, target.ID, roles: memberData.Roles.ConvertAll(r => r.ToSnowflake()),
|
||||
reason: $"({executor.GetTag()}) {reason}".EncodeHeader(), ct: ct);
|
||||
if (unmuteResult.IsSuccess)
|
||||
{
|
||||
memberData.MutedUntil = null;
|
||||
}
|
||||
|
||||
return unmuteResult;
|
||||
}
|
||||
|
||||
private async Task<Result> RemoveTimeoutAsync(
|
||||
IUser executor, IUser target, string reason, Snowflake guildId, DateTimeOffset? communicationDisabledUntil,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (communicationDisabledUntil is null)
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
|
||||
guildId, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
|
||||
communicationDisabledUntil: null, ct: ct);
|
||||
return unmuteResult;
|
||||
}
|
||||
}
|
102
TeamOctolings.Octobot/Commands/PingCommandGroup.cs
Normal file
102
TeamOctolings.Octobot/Commands/PingCommandGroup.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using System.ComponentModel;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class PingCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly IDiscordRestChannelAPI _channelApi;
|
||||
private readonly DiscordGatewayClient _client;
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public PingCommandGroup(
|
||||
IDiscordRestChannelAPI channelApi, ICommandContext context, DiscordGatewayClient client,
|
||||
GuildDataService guildData, IFeedbackService feedback, IDiscordRestUserAPI userApi)
|
||||
{
|
||||
_channelApi = channelApi;
|
||||
_context = context;
|
||||
_client = client;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows time taken for the gateway to respond to the last heartbeat.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("ping", "пинг")]
|
||||
[Description("Get bot latency")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecutePingAsync()
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
return await SendLatencyAsync(channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> SendLatencyAsync(
|
||||
Snowflake channelId, IUser bot, CancellationToken ct = default)
|
||||
{
|
||||
var latency = _client.Latency.TotalMilliseconds;
|
||||
if (latency is 0)
|
||||
{
|
||||
// No heartbeat has occurred, estimate latency from local time and "Octobot is thinking..." message
|
||||
var lastMessageResult = await _channelApi.GetChannelMessagesAsync(
|
||||
channelId, limit: 1, ct: ct);
|
||||
if (!lastMessageResult.IsDefined(out var lastMessage))
|
||||
{
|
||||
return ResultExtensions.FromError(lastMessageResult);
|
||||
}
|
||||
|
||||
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
|
||||
.WithTitle($"Generic{Random.Shared.Next(1, 4)}".Localized())
|
||||
.WithDescription($"{latency:F0}{Messages.Milliseconds}")
|
||||
.WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red)
|
||||
.WithCurrentTimestamp()
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
382
TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
Normal file
382
TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
Normal file
|
@ -0,0 +1,382 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Parsers;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles commands to manage reminders: /remind, /listremind, /delremind
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class RemindCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly IInteractionCommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly IDiscordRestInteractionAPI _interactionApi;
|
||||
|
||||
public RemindCommandGroup(
|
||||
IInteractionCommandContext context, GuildDataService guildData, IFeedbackService feedback,
|
||||
IDiscordRestUserAPI userApi, IDiscordRestInteractionAPI interactionApi)
|
||||
{
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
_interactionApi = interactionApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that lists reminders of the user that called it.
|
||||
/// </summary>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("listremind")]
|
||||
[Description("List your reminders")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteListReminderAsync()
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct)
|
||||
{
|
||||
if (data.Reminders.Count == 0)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.NoRemindersFound, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var i = 0; i < data.Reminders.Count; i++)
|
||||
{
|
||||
var reminder = data.Reminders[i];
|
||||
builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString())))
|
||||
.AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text)))
|
||||
.AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At)))
|
||||
.AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.ReminderList, executor.GetTag()), executor)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that schedules a reminder with the specified text.
|
||||
/// </summary>
|
||||
/// <param name="timeSpanString">The period of time which must pass before the reminder will be sent.</param>
|
||||
/// <param name="text">The text of the reminder.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("remind")]
|
||||
[Description("Create a reminder")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteReminderAsync(
|
||||
[Description("After what period of time mention the reminder (e.g. 1h30m)")]
|
||||
[Option("in")]
|
||||
string timeSpanString,
|
||||
[Description("Reminder text")] [MaxLength(512)]
|
||||
string text)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(timeSpanString);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await AddReminderAsync(timeSpan, text, data, channelId, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> AddReminderAsync(TimeSpan timeSpan, string text, GuildData data,
|
||||
Snowflake channelId, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
var memberData = data.GetOrCreateMemberData(executor.ID);
|
||||
var remindAt = DateTimeOffset.UtcNow.Add(timeSpan);
|
||||
var responseResult = await _interactionApi.GetOriginalInteractionResponseAsync(_context.Interaction.ApplicationID, _context.Interaction.Token, ct);
|
||||
if (!responseResult.IsDefined(out var response))
|
||||
{
|
||||
return (Result)responseResult;
|
||||
}
|
||||
|
||||
memberData.Reminders.Add(
|
||||
new Reminder
|
||||
{
|
||||
At = remindAt,
|
||||
ChannelId = channelId.Value,
|
||||
Text = text,
|
||||
MessageId = response.ID.Value
|
||||
});
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text)))
|
||||
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.ReminderCreated, executor.GetTag()), executor)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Green)
|
||||
.WithFooter(string.Format(Messages.ReminderPosition, memberData.Reminders.Count))
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
public enum Parameters
|
||||
{
|
||||
[UsedImplicitly] Time,
|
||||
[UsedImplicitly] Text
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that edits a scheduled reminder using the specified text or time.
|
||||
/// </summary>
|
||||
/// <param name="position">The list position of the reminder to edit.</param>
|
||||
/// <param name="parameter">The reminder's parameter to edit.</param>
|
||||
/// <param name="value">The new value for the reminder as a text or time.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("editremind")]
|
||||
[Description("Edit a reminder")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEditReminderAsync(
|
||||
[Description("Position in list")] [MinValue(1)]
|
||||
int position,
|
||||
[Description("Parameter to edit")] Parameters parameter,
|
||||
[Description("Parameter's new value")] string value)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(executor.ID);
|
||||
|
||||
if (parameter is Parameters.Time)
|
||||
{
|
||||
return await EditReminderTimeAsync(position - 1, value, memberData, bot, executor, CancellationToken);
|
||||
}
|
||||
|
||||
return await EditReminderTextAsync(position - 1, value, memberData, bot, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> EditReminderTimeAsync(int index, string value, MemberData data,
|
||||
IUser bot, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
if (index >= data.Reminders.Count)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(value);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var oldReminder = data.Reminders[index];
|
||||
var remindAt = DateTimeOffset.UtcNow.Add(timeSpan);
|
||||
|
||||
data.Reminders.Add(oldReminder with { At = remindAt });
|
||||
data.Reminders.RemoveAt(index);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text)))
|
||||
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count))
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private async Task<Result> EditReminderTextAsync(int index, string value, MemberData data,
|
||||
IUser bot, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
if (index >= data.Reminders.Count)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var oldReminder = data.Reminders[index];
|
||||
|
||||
data.Reminders.Add(oldReminder with { Text = value });
|
||||
data.Reminders.RemoveAt(index);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value)))
|
||||
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At)));
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count))
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that deletes a reminder using its list position.
|
||||
/// </summary>
|
||||
/// <param name="position">The list position of the reminder to delete.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("delremind")]
|
||||
[Description("Delete one of your reminders")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteDeleteReminderAsync(
|
||||
[Description("Position in list")] [MinValue(1)]
|
||||
int position)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await DeleteReminderAsync(data.GetOrCreateMemberData(executorId), position - 1, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> DeleteReminderAsync(MemberData data, int index, IUser bot,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (index >= data.Reminders.Count)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var reminder = data.Reminders[index];
|
||||
|
||||
var description = new StringBuilder()
|
||||
.AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text)))
|
||||
.AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At)));
|
||||
|
||||
data.Reminders.RemoveAt(index);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.ReminderDeleted, bot)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
309
TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
Normal file
309
TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
Normal file
|
@ -0,0 +1,309 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
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;
|
||||
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;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Data.Options;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the commands to list and modify per-guild settings: /settings and /settings list.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class SettingsCommandGroup : CommandGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents all options as an array of objects implementing <see cref="IGuildOption" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// WARNING: If you update this array in any way, you must also update <see cref="AllOptionsEnum" /> and make sure
|
||||
/// that the orders match.
|
||||
/// </remarks>
|
||||
private static readonly IGuildOption[] AllOptions =
|
||||
[
|
||||
GuildSettings.Language,
|
||||
GuildSettings.WelcomeMessage,
|
||||
GuildSettings.LeaveMessage,
|
||||
GuildSettings.ReceiveStartupMessages,
|
||||
GuildSettings.RemoveRolesOnMute,
|
||||
GuildSettings.ReturnRolesOnRejoin,
|
||||
GuildSettings.AutoStartEvents,
|
||||
GuildSettings.RenameHoistedUsers,
|
||||
GuildSettings.PublicFeedbackChannel,
|
||||
GuildSettings.PrivateFeedbackChannel,
|
||||
GuildSettings.WelcomeMessagesChannel,
|
||||
GuildSettings.EventNotificationChannel,
|
||||
GuildSettings.DefaultRole,
|
||||
GuildSettings.MuteRole,
|
||||
GuildSettings.ModeratorRole,
|
||||
GuildSettings.EventNotificationRole,
|
||||
GuildSettings.EventEarlyNotificationOffset
|
||||
];
|
||||
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
private readonly Utility _utility;
|
||||
|
||||
public SettingsCommandGroup(
|
||||
ICommandContext context, GuildDataService guildData,
|
||||
IFeedbackService feedback, IDiscordRestUserAPI userApi, Utility utility)
|
||||
{
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_userApi = userApi;
|
||||
_utility = utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that sends a page from the list of current GuildSettings.
|
||||
/// </summary>
|
||||
/// <param name="page">The number of the page to send.</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("listsettings")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||
[Description("Shows settings list for this server")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteListSettingsAsync(
|
||||
[Description("Settings list page")] [MinValue(1)]
|
||||
int page)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
return await SendSettingsListAsync(cfg, bot, page, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> SendSettingsListAsync(JsonNode cfg, IUser bot, int page,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var description = new StringBuilder();
|
||||
var footer = new StringBuilder();
|
||||
|
||||
const int optionsPerPage = 10;
|
||||
|
||||
var totalPages = (AllOptions.Length + optionsPerPage - 1) / optionsPerPage;
|
||||
var lastOptionOnPage = Math.Min(optionsPerPage * page, AllOptions.Length);
|
||||
var firstOptionOnPage = optionsPerPage * page - optionsPerPage;
|
||||
|
||||
if (firstOptionOnPage >= AllOptions.Length)
|
||||
{
|
||||
var errorEmbed = new EmbedBuilder().WithSmallTitle(Messages.PageNotFound, bot)
|
||||
.WithDescription(string.Format(Messages.PagesAllowed, Markdown.Bold(totalPages.ToString())))
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
|
||||
}
|
||||
|
||||
footer.Append($"{Messages.Page} {page}/{totalPages} ");
|
||||
for (var i = 0; i < totalPages; i++)
|
||||
{
|
||||
footer.Append(i + 1 == page ? "●" : "○");
|
||||
}
|
||||
|
||||
for (var i = firstOptionOnPage; i < lastOptionOnPage; i++)
|
||||
{
|
||||
var optionName = AllOptions[i].Name;
|
||||
var optionValue = AllOptions[i].Display(cfg);
|
||||
|
||||
description.AppendBulletPointLine($"Settings{optionName}".Localized())
|
||||
.AppendSubBulletPoint(Markdown.InlineCode(optionName))
|
||||
.Append(": ").AppendLine(optionValue);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, bot)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(ColorsList.Default)
|
||||
.WithFooter(footer.ToString())
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that modifies per-guild GuildSettings.
|
||||
/// </summary>
|
||||
/// <param name="setting">The setting to modify.</param>
|
||||
/// <param name="value">The new value of the setting.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("editsettings")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||
[Description("Change settings for this server")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEditSettingsAsync(
|
||||
[Description("The setting whose value you want to change")]
|
||||
AllOptionsEnum setting,
|
||||
[Description("Setting value")] [MaxLength(512)]
|
||||
string value)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await EditSettingAsync(AllOptions[(int)setting], value, data, channelId, executor, bot,
|
||||
CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> EditSettingAsync(
|
||||
IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var setResult = option.Set(data.Settings, value);
|
||||
if (!setResult.IsSuccess)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot)
|
||||
.WithDescription(setResult.Error.Message)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append(Markdown.InlineCode(option.Name))
|
||||
.Append($" {Messages.SettingIsNow} ")
|
||||
.Append(option.Display(data.Settings));
|
||||
var title = Messages.SettingSuccessfullyChanged;
|
||||
var description = builder.ToString();
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, bot, ColorsList.Magenta, false, ct);
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
|
||||
.WithDescription(description)
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that resets per-guild GuildSettings.
|
||||
/// </summary>
|
||||
/// <param name="setting">The setting to reset.</param>
|
||||
/// <returns>A feedback sending result which may have succeeded.</returns>
|
||||
[Command("resetsettings")]
|
||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
|
||||
[Description("Reset settings for this guild")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteResetSettingsAsync(
|
||||
[Description("Setting to reset")] AllOptionsEnum? setting = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
|
||||
if (setting is not null)
|
||||
{
|
||||
return await ResetSingleSettingAsync(cfg, bot, AllOptions[(int)setting], CancellationToken);
|
||||
}
|
||||
|
||||
return await ResetAllSettingsAsync(cfg, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> ResetSingleSettingAsync(JsonNode cfg, IUser bot,
|
||||
IGuildOption option, CancellationToken ct = default)
|
||||
{
|
||||
var resetResult = option.Reset(cfg);
|
||||
if (!resetResult.IsSuccess)
|
||||
{
|
||||
return ResultExtensions.FromError(resetResult);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.SingleSettingReset, option.Name), bot)
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private async Task<Result> ResetAllSettingsAsync(JsonNode cfg, IUser bot,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var failedResults = new List<Result>();
|
||||
foreach (var resetResult in AllOptions.Select(option => option.Reset(cfg)))
|
||||
{
|
||||
failedResults.AddIfFailed(resetResult);
|
||||
}
|
||||
|
||||
if (failedResults.Count is not 0)
|
||||
{
|
||||
return failedResults.AggregateErrors();
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(Messages.AllSettingsReset, bot)
|
||||
.WithColour(ColorsList.Green)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
561
TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
Normal file
561
TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
Normal file
|
@ -0,0 +1,561 @@
|
|||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
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.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using TeamOctolings.Octobot.Data;
|
||||
using TeamOctolings.Octobot.Extensions;
|
||||
using TeamOctolings.Octobot.Parsers;
|
||||
using TeamOctolings.Octobot.Services;
|
||||
|
||||
namespace TeamOctolings.Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ToolsCommandGroup : CommandGroup
|
||||
{
|
||||
private readonly ICommandContext _context;
|
||||
private readonly IFeedbackService _feedback;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
private readonly GuildDataService _guildData;
|
||||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public ToolsCommandGroup(
|
||||
ICommandContext context, IFeedbackService feedback,
|
||||
GuildDataService guildData, IDiscordRestGuildAPI guildApi,
|
||||
IDiscordRestUserAPI userApi)
|
||||
{
|
||||
_context = context;
|
||||
_guildData = guildData;
|
||||
_feedback = feedback;
|
||||
_guildApi = guildApi;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows information about user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Information in the output:
|
||||
/// <list type="bullet">
|
||||
/// <item>Display name</item>
|
||||
/// <item>Discord user since</item>
|
||||
/// <item>Guild nickname</item>
|
||||
/// <item>Guild member since</item>
|
||||
/// <item>Nitro booster since</item>
|
||||
/// <item>Guild roles</item>
|
||||
/// <item>Active mute information</item>
|
||||
/// <item>Active ban information</item>
|
||||
/// <item>Is on guild status</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="target">The user to show info about.</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("userinfo")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Shows info about user")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteUserInfoAsync(
|
||||
[Description("User to show info about")]
|
||||
IUser? target = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await ShowUserInfoAsync(target ?? executor, bot, data, guildId, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> ShowUserInfoAsync(
|
||||
IUser target, IUser bot, GuildData data, Snowflake guildId, CancellationToken ct = default)
|
||||
{
|
||||
var builder = new StringBuilder().AppendLine($"### <@{target.ID}>");
|
||||
|
||||
if (target.GlobalName.IsDefined(out var globalName))
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoDisplayName)
|
||||
.AppendLine(Markdown.InlineCode(globalName));
|
||||
}
|
||||
|
||||
builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince)
|
||||
.AppendLine(Markdown.Timestamp(target.ID.Timestamp));
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(target.ID);
|
||||
|
||||
var embedColor = ColorsList.Cyan;
|
||||
|
||||
var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, target.ID, ct);
|
||||
DateTimeOffset? communicationDisabledUntil = null;
|
||||
if (guildMemberResult.IsDefined(out var guildMember))
|
||||
{
|
||||
communicationDisabledUntil = guildMember.CommunicationDisabledUntil.OrDefault(null);
|
||||
|
||||
embedColor = AppendGuildInformation(embedColor, guildMember, builder);
|
||||
}
|
||||
|
||||
var wasMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) ||
|
||||
communicationDisabledUntil is not null;
|
||||
var wasBanned = memberData.BannedUntil is not null;
|
||||
var wasKicked = memberData.Kicked;
|
||||
|
||||
if (wasMuted || wasBanned || wasKicked)
|
||||
{
|
||||
builder.Append("### ")
|
||||
.AppendLine(Markdown.Bold(Messages.UserInfoPunishments));
|
||||
|
||||
embedColor = AppendPunishmentsInformation(wasMuted, wasKicked, wasBanned, memberData,
|
||||
builder, embedColor, communicationDisabledUntil);
|
||||
}
|
||||
|
||||
if (!guildMemberResult.IsSuccess && !wasBanned)
|
||||
{
|
||||
builder.Append("### ")
|
||||
.AppendLine(Markdown.Bold(Messages.UserInfoNotOnGuild));
|
||||
|
||||
embedColor = ColorsList.Default;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.InformationAbout, target.GetTag()), bot)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(embedColor)
|
||||
.WithLargeUserAvatar(target)
|
||||
.WithFooter($"ID: {target.ID.ToString()}")
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private static Color AppendPunishmentsInformation(bool wasMuted, bool wasKicked, bool wasBanned,
|
||||
MemberData memberData, StringBuilder builder, Color embedColor, DateTimeOffset? communicationDisabledUntil)
|
||||
{
|
||||
if (wasMuted)
|
||||
{
|
||||
AppendMuteInformation(memberData, communicationDisabledUntil, builder);
|
||||
embedColor = ColorsList.Red;
|
||||
}
|
||||
|
||||
if (wasKicked)
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoKicked);
|
||||
}
|
||||
|
||||
if (wasBanned)
|
||||
{
|
||||
AppendBanInformation(memberData, builder);
|
||||
embedColor = ColorsList.Black;
|
||||
}
|
||||
|
||||
return embedColor;
|
||||
}
|
||||
|
||||
private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder)
|
||||
{
|
||||
if (guildMember.Nickname.IsDefined(out var nickname))
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoGuildNickname)
|
||||
.AppendLine(Markdown.InlineCode(nickname));
|
||||
}
|
||||
|
||||
builder.AppendBulletPointLine(Messages.UserInfoGuildMemberSince)
|
||||
.AppendLine(Markdown.Timestamp(guildMember.JoinedAt));
|
||||
|
||||
if (guildMember.PremiumSince.IsDefined(out var premiumSince))
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoGuildMemberPremiumSince)
|
||||
.AppendLine(Markdown.Timestamp(premiumSince.Value));
|
||||
color = ColorsList.Magenta;
|
||||
}
|
||||
|
||||
if (guildMember.Roles.Count > 0)
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoGuildRoles);
|
||||
for (var i = 0; i < guildMember.Roles.Count - 1; i++)
|
||||
{
|
||||
builder.Append($"<@&{guildMember.Roles[i]}>, ");
|
||||
}
|
||||
|
||||
builder.AppendLine($"<@&{guildMember.Roles[^1]}>");
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private static void AppendBanInformation(MemberData memberData, StringBuilder builder)
|
||||
{
|
||||
if (memberData.BannedUntil < DateTimeOffset.MaxValue)
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoBanned)
|
||||
.AppendSubBulletPointLine(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value)));
|
||||
return;
|
||||
}
|
||||
|
||||
builder.AppendBulletPointLine(Messages.UserInfoBannedPermanently);
|
||||
}
|
||||
|
||||
private static void AppendMuteInformation(
|
||||
MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder)
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoMuted);
|
||||
if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil)
|
||||
{
|
||||
builder.AppendSubBulletPointLine(Messages.UserInfoMutedByMuteRole)
|
||||
.AppendSubBulletPointLine(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value)));
|
||||
}
|
||||
|
||||
if (communicationDisabledUntil is not null)
|
||||
{
|
||||
builder.AppendSubBulletPointLine(Messages.UserInfoMutedByTimeout)
|
||||
.AppendSubBulletPointLine(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows guild information.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Information in the output:
|
||||
/// <list type="bullet">
|
||||
/// <item>Guild description</item>
|
||||
/// <item>Creation date</item>
|
||||
/// <item>Guild's language</item>
|
||||
/// <item>Guild's owner</item>
|
||||
/// <item>Boost level</item>
|
||||
/// <item>Boost count</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("guildinfo")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Shows info about current guild")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteGuildInfoAsync()
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
|
||||
if (!guildResult.IsDefined(out var guild))
|
||||
{
|
||||
return ResultExtensions.FromError(guildResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await ShowGuildInfoAsync(bot, guild, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct)
|
||||
{
|
||||
var description = new StringBuilder().AppendLine($"## {guild.Name}");
|
||||
|
||||
if (guild.Description is not null)
|
||||
{
|
||||
description.AppendBulletPointLine(Messages.GuildInfoDescription)
|
||||
.AppendLine(Markdown.InlineCode(guild.Description));
|
||||
}
|
||||
|
||||
description.AppendBulletPointLine(Messages.GuildInfoCreatedAt)
|
||||
.AppendLine(Markdown.Timestamp(guild.ID.Timestamp))
|
||||
.AppendBulletPointLine(Messages.GuildInfoOwner)
|
||||
.AppendLine(Mention.User(guild.OwnerID));
|
||||
|
||||
var embedColor = ColorsList.Cyan;
|
||||
|
||||
if (guild.PremiumTier > PremiumTier.None)
|
||||
{
|
||||
description.Append("### ").AppendLine(Messages.GuildInfoServerBoost)
|
||||
.AppendBulletPoint(Messages.GuildInfoBoostTier)
|
||||
.Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumTier.ToString()))
|
||||
.AppendBulletPoint(Messages.GuildInfoBoostCount)
|
||||
.Append(": ").AppendLine(Markdown.InlineCode(guild.PremiumSubscriptionCount.ToString()));
|
||||
embedColor = ColorsList.Magenta;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.InformationAbout, guild.Name), bot)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(embedColor)
|
||||
.WithLargeGuildIcon(guild)
|
||||
.WithGuildBanner(guild)
|
||||
.WithFooter($"ID: {guild.ID.ToString()}")
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that generates a random number using maximum and minimum numbers.
|
||||
/// </summary>
|
||||
/// <param name="first">The first number used for randomization.</param>
|
||||
/// <param name="second">The second number used for randomization. Default value: 0</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("random")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Generates a random number")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteRandomAsync(
|
||||
[Description("First number")] long first,
|
||||
[Description("Second number (Default: 0)")]
|
||||
long? second = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await SendRandomNumberAsync(first, second, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> SendRandomNumberAsync(long first, long? secondNullable,
|
||||
IUser executor, CancellationToken ct)
|
||||
{
|
||||
const long secondDefault = 0;
|
||||
var second = secondNullable ?? secondDefault;
|
||||
|
||||
var min = Math.Min(first, second);
|
||||
var max = Math.Max(first, second);
|
||||
|
||||
var i = Random.Shared.NextInt64(min, max + 1);
|
||||
|
||||
var description = new StringBuilder().Append("# ").Append(i);
|
||||
|
||||
description.AppendLine().AppendBulletPoint(string.Format(
|
||||
Messages.RandomMin, Markdown.InlineCode(min.ToString())));
|
||||
if (secondNullable is null && first >= secondDefault)
|
||||
{
|
||||
description.Append(' ').Append(Messages.Default);
|
||||
}
|
||||
|
||||
description.AppendLine().AppendBulletPoint(string.Format(
|
||||
Messages.RandomMax, Markdown.InlineCode(max.ToString())));
|
||||
if (secondNullable is null && first < secondDefault)
|
||||
{
|
||||
description.Append(' ').Append(Messages.Default);
|
||||
}
|
||||
|
||||
var embedColor = ColorsList.Blue;
|
||||
if (secondNullable is not null && min == max)
|
||||
{
|
||||
description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame));
|
||||
embedColor = ColorsList.Red;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.RandomTitle, executor.GetTag()), executor)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(embedColor)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private static readonly TimestampStyle[] AllStyles =
|
||||
[
|
||||
TimestampStyle.ShortDate,
|
||||
TimestampStyle.LongDate,
|
||||
TimestampStyle.ShortTime,
|
||||
TimestampStyle.LongTime,
|
||||
TimestampStyle.ShortDateTime,
|
||||
TimestampStyle.LongDateTime,
|
||||
TimestampStyle.RelativeTime
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.
|
||||
/// </summary>
|
||||
/// <param name="stringOffset">The offset for the current timestamp.</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("timestamp")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Shows a timestamp in all styles")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteTimestampAsync(
|
||||
[Description("Offset from current time")] [Option("offset")]
|
||||
string? stringOffset = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return ResultExtensions.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
if (stringOffset is null)
|
||||
{
|
||||
return await SendTimestampAsync(null, executor, CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(stringOffset);
|
||||
if (!parseResult.IsDefined(out var offset))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await SendTimestampAsync(offset, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private Task<Result> SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds();
|
||||
|
||||
var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString());
|
||||
|
||||
if (offset is not null)
|
||||
{
|
||||
description.AppendLine(string.Format(
|
||||
Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine();
|
||||
}
|
||||
|
||||
foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style)))
|
||||
{
|
||||
description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp))
|
||||
.Append(" → ").AppendLine(markdownTimestamp);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.TimestampTitle, executor.GetTag()), executor)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(ColorsList.Blue)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows a random answer from the Magic 8-Ball.
|
||||
/// </summary>
|
||||
/// <param name="question">Unused input.</param>
|
||||
/// <remarks>
|
||||
/// The 8-Ball answers were taken from <a href="https://en.wikipedia.org/wiki/Magic_8_Ball#Possible_answers">Wikipedia</a>.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("8ball")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Ask the Magic 8-Ball a question")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEightBallAsync(
|
||||
// let the user think he's actually asking the ball a question
|
||||
[Description("Question to ask")] string question)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return ResultExtensions.FromError(botResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await AnswerEightBallAsync(bot, CancellationToken);
|
||||
}
|
||||
|
||||
private static readonly string[] AnswerTypes =
|
||||
[
|
||||
"Positive", "Questionable", "Neutral", "Negative"
|
||||
];
|
||||
|
||||
private Task<Result> AnswerEightBallAsync(IUser bot, CancellationToken ct)
|
||||
{
|
||||
var typeNumber = Random.Shared.Next(0, 4);
|
||||
var embedColor = typeNumber switch
|
||||
{
|
||||
0 => ColorsList.Blue,
|
||||
1 => ColorsList.Green,
|
||||
2 => ColorsList.Yellow,
|
||||
3 => ColorsList.Red,
|
||||
_ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber))
|
||||
};
|
||||
|
||||
var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized();
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(answer, bot)
|
||||
.WithColour(embedColor)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue