diff --git a/locale/Messages.resx b/locale/Messages.resx
index 84d7fa7..3d05722 100644
--- a/locale/Messages.resx
+++ b/locale/Messages.resx
@@ -600,4 +600,46 @@
All settings have been reset
+
+ Display name
+
+
+ Information about {0}
+
+
+ Muted
+
+
+ Discord user since
+
+
+ Banned
+
+
+ Punishments
+
+
+ Banned permanently
+
+
+ Not in the guild
+
+
+ Muted by timeout
+
+
+ Muted by mute role
+
+
+ Guild member since
+
+
+ Nickname
+
+
+ Roles
+
+
+ Nitro booster since
+
diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx
index abeaf88..3f20dd0 100644
--- a/locale/Messages.ru.resx
+++ b/locale/Messages.ru.resx
@@ -600,4 +600,46 @@
Все настройки были сброшены
+
+ Отображаемое имя
+
+
+ Информация о {0}
+
+
+ Заглушен
+
+
+ Вступил в Discord
+
+
+ Забанен
+
+
+ Наказания
+
+
+ Забанен навсегда
+
+
+ Не на сервере
+
+
+ Заглушен с помощью тайм-аута
+
+
+ Заглушен с помощью роли мута
+
+
+ Вступил на сервер
+
+
+ Никнейм
+
+
+ Роли
+
+
+ Начал бустить сервер
+
diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx
index fb01e72..a564e1c 100644
--- a/locale/Messages.tt-ru.resx
+++ b/locale/Messages.tt-ru.resx
@@ -600,4 +600,46 @@
откатываемся к заводским...
+
+ дисплейнейм
+
+
+ деанон {0}
+
+
+ замучен
+
+
+ юзер Discord со времен
+
+
+ забанен
+
+
+ приколы полученные по заслугам
+
+
+ забанен
+
+
+ вышел из сервера
+
+
+ замучен таймаутом
+
+
+ замучен ролькой
+
+
+ участник сервера со времен
+
+
+ сервернейм
+
+
+ рольки
+
+
+ бустит сервер со времен
+
diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs
index 246869e..4ba43f8 100644
--- a/src/Boyfriend.cs
+++ b/src/Boyfriend.cs
@@ -102,7 +102,8 @@ public sealed class Boyfriend
.WithCommandGroup()
.WithCommandGroup()
.WithCommandGroup()
- .WithCommandGroup();
+ .WithCommandGroup()
+ .WithCommandGroup();
var responderTypes = typeof(Boyfriend).Assembly
.GetExportedTypes()
.Where(t => t.IsResponder());
diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs
new file mode 100644
index 0000000..3ac8b70
--- /dev/null
+++ b/src/Commands/ToolsCommandGroup.cs
@@ -0,0 +1,231 @@
+using System.ComponentModel;
+using System.Drawing;
+using System.Text;
+using Boyfriend.Data;
+using Boyfriend.Services;
+using JetBrains.Annotations;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
+using Remora.Discord.Commands.Attributes;
+using Remora.Discord.Commands.Contexts;
+using Remora.Discord.Commands.Feedback.Services;
+using Remora.Discord.Extensions.Embeds;
+using Remora.Discord.Extensions.Formatting;
+using Remora.Rest.Core;
+using Remora.Results;
+
+namespace Boyfriend.Commands;
+
+///
+/// Handles commands related to tools: /showinfo.
+///
+[UsedImplicitly]
+public class ToolsCommandGroup : CommandGroup
+{
+ private readonly ICommandContext _context;
+ private readonly FeedbackService _feedback;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ private readonly GuildDataService _guildData;
+ private readonly IDiscordRestUserAPI _userApi;
+
+ public ToolsCommandGroup(
+ ICommandContext context, FeedbackService 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("showinfo")]
+ [DiscordDefaultDMPermission(false)]
+ [Description("Shows info about user")]
+ [UsedImplicitly]
+ public async Task ExecuteShowInfoAsync(
+ [Description("User to show info about")]
+ IUser? target = null)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out _, out var userId))
+ {
+ return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
+ }
+
+ var userResult = await _userApi.GetUserAsync(userId, CancellationToken);
+ if (!userResult.IsDefined(out var user))
+ {
+ return Result.FromError(userResult);
+ }
+
+ var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken);
+ if (!currentUserResult.IsDefined(out var currentUser))
+ {
+ return Result.FromError(currentUserResult);
+ }
+
+ var data = await _guildData.GetData(guildId, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ return await ShowUserInfoAsync(target ?? user, currentUser, data, guildId, CancellationToken);
+ }
+
+ private async Task ShowUserInfoAsync(
+ IUser user, IUser currentUser, GuildData data, Snowflake guildId, CancellationToken ct = default)
+ {
+ var builder = new StringBuilder().AppendLine($"### <@{user.ID}>");
+
+ if (user.GlobalName is not null)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoDisplayName)
+ .AppendLine(Markdown.InlineCode(user.GlobalName));
+ }
+
+ builder.Append("- ").AppendLine(Messages.ShowInfoDiscordUserSince)
+ .AppendLine(Markdown.Timestamp(user.ID.Timestamp));
+
+ var memberData = data.GetOrCreateMemberData(user.ID);
+
+ var embedColor = ColorsList.Cyan;
+
+ var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, user.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, user.ID, ct);
+
+ if (isMuted || existingBanResult.IsDefined())
+ {
+ builder.Append("### ")
+ .AppendLine(Markdown.Bold(Messages.ShowInfoPunishments));
+ }
+
+ 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.ShowInfoNotOnGuild));
+
+ embedColor = ColorsList.Default;
+ }
+
+ var embed = new EmbedBuilder().WithSmallTitle(
+ string.Format(Messages.ShowInfoTitle, user.GetTag()), currentUser)
+ .WithDescription(builder.ToString())
+ .WithColour(embedColor)
+ .WithLargeAvatar(user)
+ .WithFooter($"ID: {user.ID.ToString()}")
+ .Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct);
+ }
+
+ private static Color AppendGuildInformation(Color color, IGuildMember guildMember, StringBuilder builder)
+ {
+ if (guildMember.Nickname.IsDefined(out var nickname))
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoGuildNickname)
+ .AppendLine(Markdown.InlineCode(nickname));
+ }
+
+ builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberSince)
+ .AppendLine(Markdown.Timestamp(guildMember.JoinedAt));
+
+ if (guildMember.PremiumSince.IsDefined(out var premiumSince))
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoGuildMemberPremiumSince)
+ .AppendLine(Markdown.Timestamp(premiumSince.Value));
+ color = ColorsList.Magenta;
+ }
+
+ if (guildMember.Roles.Count > 0)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoGuildRoles);
+ 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.Append("- ").AppendLine(Messages.ShowInfoBanned)
+ .Append(" - ").AppendLine(string.Format(
+ Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.BannedUntil.Value)));
+ return;
+ }
+
+ builder.Append("- ").AppendLine(Messages.ShowInfoBannedPermanently);
+ }
+
+ private static void AppendMuteInformation(
+ MemberData memberData, DateTimeOffset? communicationDisabledUntil, StringBuilder builder)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoMuted);
+ if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil)
+ {
+ builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByMuteRole)
+ .Append(" - ").AppendLine(string.Format(
+ Messages.DescriptionActionExpiresAt, Markdown.Timestamp(memberData.MutedUntil.Value)));
+ }
+
+ if (communicationDisabledUntil is not null)
+ {
+ builder.Append(" - ").AppendLine(Messages.ShowInfoMutedByTimeout)
+ .Append(" - ").AppendLine(string.Format(
+ Messages.DescriptionActionExpiresAt, Markdown.Timestamp(communicationDisabledUntil.Value)));
+ }
+ }
+}
diff --git a/src/Extensions.cs b/src/Extensions.cs
index 9a05e80..1e30cf4 100644
--- a/src/Extensions.cs
+++ b/src/Extensions.cs
@@ -74,6 +74,23 @@ public static class Extensions
return builder;
}
+ ///
+ /// Adds a user avatar in the thumbnail field.
+ ///
+ /// The builder to add the thumbnail to.
+ /// The user whose avatar to use in the thumbnail field.
+ /// The builder with the added avatar in the thumbnail field.
+ public static EmbedBuilder WithLargeAvatar(
+ this EmbedBuilder builder, IUser avatarSource)
+ {
+ var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
+ var avatarUrl = avatarUrlResult.IsSuccess
+ ? avatarUrlResult.Entity
+ : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
+
+ return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri);
+ }
+
///
/// Adds a footer representing that the action was performed in the .
///
diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs
index b45ae70..884cfd4 100644
--- a/src/Messages.Designer.cs
+++ b/src/Messages.Designer.cs
@@ -1013,5 +1013,89 @@ namespace Boyfriend {
return ResourceManager.GetString("AllSettingsReset", resourceCulture);
}
}
+
+ internal static string ShowInfoTitle {
+ get {
+ return ResourceManager.GetString("ShowInfoTitle", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoDisplayName {
+ get {
+ return ResourceManager.GetString("ShowInfoDisplayName", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoDiscordUserSince {
+ get {
+ return ResourceManager.GetString("ShowInfoDiscordUserSince", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMuted {
+ get {
+ return ResourceManager.GetString("ShowInfoMuted", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoBanned {
+ get {
+ return ResourceManager.GetString("ShowInfoBanned", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoPunishments {
+ get {
+ return ResourceManager.GetString("ShowInfoPunishments", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoBannedPermanently {
+ get {
+ return ResourceManager.GetString("ShowInfoBannedPermanently", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoNotOnGuild {
+ get {
+ return ResourceManager.GetString("ShowInfoNotOnGuild", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMutedByTimeout {
+ get {
+ return ResourceManager.GetString("ShowInfoMutedByTimeout", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMutedByMuteRole {
+ get {
+ return ResourceManager.GetString("ShowInfoMutedByMuteRole", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoGuildMemberSince {
+ get {
+ return ResourceManager.GetString("ShowInfoGuildMemberSince", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoGuildNickname {
+ get {
+ return ResourceManager.GetString("ShowInfoGuildNickname", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoGuildRoles {
+ get {
+ return ResourceManager.GetString("ShowInfoGuildRoles", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoGuildMemberPremiumSince {
+ get {
+ return ResourceManager.GetString("ShowInfoGuildMemberPremiumSince", resourceCulture);
+ }
+ }
}
}