diff --git a/locale/Messages.resx b/locale/Messages.resx
index 84d7fa7..7d6b9e0 100644
--- a/locale/Messages.resx
+++ b/locale/Messages.resx
@@ -600,4 +600,43 @@
All settings have been reset
+
+ Display Name
+
+
+ Not present
+
+
+ Information about {0}
+
+
+ Muted
+
+
+ Discord user since
+
+
+ Mentioned as
+
+
+ Banned
+
+
+ Punishments
+
+
+ Banned permanently
+
+
+ Not on the server
+
+
+ Muted with a timeout
+
+
+ Muted with a mute role
+
+
+ Until
+
diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx
index abeaf88..56722cf 100644
--- a/locale/Messages.ru.resx
+++ b/locale/Messages.ru.resx
@@ -600,4 +600,43 @@
Все настройки были сброшены
+
+ Отображаемое имя
+
+
+ Отсутствует
+
+
+ Информация о {0}
+
+
+ Заглушен
+
+
+ Пользователь Discord с
+
+
+ Упоминается как
+
+
+ Забанен
+
+
+ Наказания
+
+
+ Забанен навсегда
+
+
+ Не на сервере
+
+
+ Заглушен с помощью тайм-аута
+
+
+ Заглушен с помощью мут роли
+
+
+ До
+
diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx
index fb01e72..5bce0c6 100644
--- a/locale/Messages.tt-ru.resx
+++ b/locale/Messages.tt-ru.resx
@@ -600,4 +600,43 @@
откатываемся к заводским...
+
+ дисплейнейм
+
+
+ нету такого
+
+
+ деанон {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..217528e
--- /dev/null
+++ b/src/Commands/ToolsCommandGroup.cs
@@ -0,0 +1,175 @@
+using System.ComponentModel;
+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;
+
+[UsedImplicitly]
+public class ToolsCommandGroup : CommandGroup
+{
+ private readonly ICommandContext _context;
+ private readonly FeedbackService _feedback;
+ private readonly GuildDataService _guildData;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ 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;
+ }
+
+ [Command("showinfo")]
+ [DiscordDefaultDMPermission(false)]
+ [Description("Shows info about user")]
+ [UsedImplicitly]
+ public async Task ExecuteShowInfoAsync(
+ [Description("Specific user or ID 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);
+
+ if (target is not null)
+ {
+ return await ShowUserInfoAsync(target, currentUser, data, guildId, CancellationToken);
+ }
+
+ return await ShowUserInfoAsync(user, currentUser, data, guildId, CancellationToken);
+ }
+
+ private async Task ShowUserInfoAsync(
+ IUser user, IUser currentUser, GuildData data, Snowflake guildId, CancellationToken ct = default)
+ {
+ var embedColor = ColorsList.Cyan;
+
+ var memberData = data.GetOrCreateMemberData(user.ID);
+
+ var guildMemberResult = await _guildApi.GetGuildMemberAsync(guildId, user.ID, ct);
+ guildMemberResult.IsDefined(out var guildMember);
+
+ var isMuted = (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil) ||
+ (guildMember is not null && guildMember.CommunicationDisabledUntil.IsDefined());
+
+ var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, user.ID, ct);
+
+ var builder = new StringBuilder().AppendLine($"### <@{user.ID}>");
+
+ if (user.GlobalName is not null)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoDisplayName)
+ .Append(" - ").AppendLine(Markdown.Sanitize(user.GlobalName));
+ }
+
+ builder.Append("- ").AppendLine(Messages.ShowInfoDiscordUserSince)
+ .Append(" - ").AppendLine(Markdown.Timestamp(user.ID.Timestamp));
+
+ if (isMuted || existingBanResult.IsDefined())
+ {
+ builder.Append("### ")
+ .AppendLine(Markdown.Bold(Messages.ShowInfoPunishments));
+ }
+
+ if (isMuted)
+ {
+ ShowInfoMutedUntilAsync(memberData, guildMember, builder);
+
+ embedColor = ColorsList.Red;
+ }
+
+ if (existingBanResult.IsDefined())
+ {
+ ShowInfoBannedUntilAsync(memberData, builder);
+
+ embedColor = ColorsList.Black;
+ }
+
+ if (!guildMemberResult.IsSuccess && !existingBanResult.IsDefined())
+ {
+ builder.Append("### ")
+ .AppendLine(Markdown.Bold(Messages.ShowInfoNotOnServer));
+
+ 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 void ShowInfoBannedUntilAsync(MemberData memberData, StringBuilder builder)
+ {
+ if (memberData.BannedUntil < DateTimeOffset.MaxValue)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoBanned)
+ .Append(" - ").Append(Messages.ShowInfoUntil).Append(' ')
+ .AppendLine(Markdown.Timestamp(memberData.BannedUntil.Value));
+ }
+
+ if (memberData.BannedUntil >= DateTimeOffset.MaxValue)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoBannedPermanently);
+ }
+ }
+
+ private static void ShowInfoMutedUntilAsync(
+ MemberData memberData, IGuildMember? guildMember, StringBuilder builder)
+ {
+ builder.Append("- ").AppendLine(Messages.ShowInfoMuted);
+ if (memberData.MutedUntil is not null && DateTimeOffset.UtcNow <= memberData.MutedUntil)
+ {
+ builder.Append(" - ").AppendLine(Messages.ShowInfoMutedWithMuteRole)
+ .Append(" - ").Append(Messages.ShowInfoUntil).Append(' ')
+ .AppendLine(Markdown.Timestamp(memberData.MutedUntil.Value));
+ }
+
+ if (guildMember is not null && guildMember.CommunicationDisabledUntil.IsDefined())
+ {
+ builder.Append(" - ").AppendLine(Messages.ShowInfoMutedWithTimeout)
+ .Append(" - ").Append(Messages.ShowInfoUntil).Append(' ')
+ .AppendLine(Markdown.Timestamp(guildMember.CommunicationDisabledUntil.Value.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..29f4dc5 100644
--- a/src/Messages.Designer.cs
+++ b/src/Messages.Designer.cs
@@ -1013,5 +1013,83 @@ namespace Boyfriend {
return ResourceManager.GetString("AllSettingsReset", resourceCulture);
}
}
+
+ internal static string NotPresent {
+ get {
+ return ResourceManager.GetString("NotPresent", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoTitle {
+ get {
+ return ResourceManager.GetString("ShowInfoTitle", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMentionedAs {
+ get {
+ return ResourceManager.GetString("ShowInfoMentionedAs", 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 ShowInfoNotOnServer {
+ get {
+ return ResourceManager.GetString("ShowInfoNotOnServer", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMutedWithTimeout {
+ get {
+ return ResourceManager.GetString("ShowInfoMutedWithTimeout", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoMutedWithMuteRole {
+ get {
+ return ResourceManager.GetString("ShowInfoMutedWithMuteRole", resourceCulture);
+ }
+ }
+
+ internal static string ShowInfoUntil {
+ get {
+ return ResourceManager.GetString("ShowInfoUntil", resourceCulture);
+ }
+ }
}
}