using System.ComponentModel; using System.Drawing; using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; using Octobot.Services; 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; namespace Octobot.Commands; /// /// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp. /// [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, IDiscordRestChannelAPI channelApi) { _context = context; _guildData = guildData; _feedback = feedback; _guildApi = guildApi; _userApi = userApi; } /// /// A slash command that shows information about user. /// /// /// Information in the output: /// /// Display name /// Discord user since /// Guild nickname /// Guild member since /// Nitro booster since /// Guild roles /// Active mute information /// Active ban information /// Is on guild status /// /// /// The user to show info about. /// /// A feedback sending result which may or may not have succeeded. /// [Command("userinfo")] [DiscordDefaultDMPermission(false)] [Description("Shows info about user")] [UsedImplicitly] public async Task 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 Result.FromError(botResult); } var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { return Result.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 ShowUserInfoAsync( IUser target, IUser bot, GuildData data, Snowflake guildId, CancellationToken ct = default) { var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); if (target.GlobalName is not null) { builder.AppendBulletPointLine(Messages.UserInfoDisplayName) .AppendLine(Markdown.InlineCode(target.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 isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) || communicationDisabledUntil is not null; var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, target.ID, ct); if (isMuted || existingBanResult.IsDefined()) { builder.Append("### ") .AppendLine(Markdown.Bold(Messages.UserInfoPunishments)); } if (isMuted) { AppendMuteInformation(memberData, communicationDisabledUntil, builder); embedColor = ColorsList.Red; } if (existingBanResult.IsDefined()) { AppendBanInformation(memberData, builder); embedColor = ColorsList.Black; } if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined()) { 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 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))); } } /// /// A slash command that shows guild information. /// /// /// Information in the output: /// /// Guild description /// Creation date /// Guild's language /// Guild's owner /// Boost level /// Boost count /// /// /// /// A feedback sending result which may or may not have succeeded. /// [Command("guildinfo")] [DiscordDefaultDMPermission(false)] [Description("Shows info current guild")] [UsedImplicitly] public async Task 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 Result.FromError(botResult); } var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); if (!guildResult.IsDefined(out var guild)) { return Result.FromError(guildResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await ShowGuildInfoAsync(bot, guild, CancellationToken); } private Task 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); } /// /// A slash command that generates a random number using maximum and minimum numbers. /// /// The first number used for randomization. /// The second number used for randomization. Default value: 0 /// /// A feedback sending result which may or may not have succeeded. /// [Command("random")] [DiscordDefaultDMPermission(false)] [Description("Generates a random number")] [UsedImplicitly] public async Task 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 Result.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 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 ]; /// /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. /// /// The offset for the current timestamp. /// /// A feedback sending result which may or may not have succeeded. /// [Command("timestamp")] [DiscordDefaultDMPermission(false)] [Description("Shows a timestamp in all styles")] [UsedImplicitly] public async Task ExecuteTimestampAsync( [Description("Offset from current time")] TimeSpan? offset = 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 Result.FromError(executorResult); } var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); return await SendTimestampAsync(offset, executor, CancellationToken); } private Task 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); } }