diff --git a/Boyfriend.cs b/Boyfriend.cs index ecbd911..8b63b52 100644 --- a/Boyfriend.cs +++ b/Boyfriend.cs @@ -1,8 +1,11 @@ -using Boyfriend.Data.Services; +using Boyfriend.Commands; +using Boyfriend.Services; +using Boyfriend.Services.Data; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Gateway.Commands; @@ -26,16 +29,10 @@ public class Boyfriend { public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; - var configuration = services.GetRequiredService(); - - - var slashService = services.GetRequiredService(); - var updateSlash = await slashService.UpdateSlashCommandsAsync(new Snowflake(1115043975573811250)); - if (!updateSlash.IsSuccess) { - Console.WriteLine("Failed to update slash commands: {Reason}", updateSlash.Error.Message); - } + _ = await slashService.UpdateSlashCommandsAsync(); + await host.RunAsync(); } @@ -71,10 +68,13 @@ public class Boyfriend { services.AddTransient() .AddDiscordCaching() - .AddDiscordCommands() + .AddDiscordCommands(true) .AddInteractivity() .AddInteractionGroup() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddCommandTree() + .WithCommandGroup(); var responderTypes = typeof(Boyfriend).Assembly .GetExportedTypes() .Where(t => t.IsResponder()); @@ -86,8 +86,4 @@ public class Boyfriend { .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) ); } - - public static string GetLocalized(string key) { - return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key; - } } diff --git a/Commands/BanCommand.cs b/Commands/BanCommand.cs index 131eb4c..a8aa777 100644 --- a/Commands/BanCommand.cs +++ b/Commands/BanCommand.cs @@ -1,36 +1,123 @@ using System.ComponentModel; +using System.Drawing; +using Boyfriend.Services; +using Boyfriend.Services.Data; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Gateway.Events; +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.Extensions; using Remora.Discord.Commands.Feedback.Services; -using Remora.Rest.Core; +using Remora.Discord.Extensions.Embeds; using Remora.Results; namespace Boyfriend.Commands; -public class BanCommand : CommandGroup{ +public class BanCommand : CommandGroup { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly GuildDataService _dataService; private readonly FeedbackService _feedbackService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + private readonly UtilityService _utility; - /// - /// Initializes a new instance of the class. - /// - /// The feedback service. - public BanCommand(FeedbackService feedbackService) - { + public BanCommand( + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + FeedbackService feedbackService, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + UtilityService utility) { + _context = context; + _channelApi = channelApi; + _dataService = dataService; _feedbackService = feedbackService; + _guildApi = guildApi; + _userApi = userApi; + _utility = utility; } - [Command("ban")] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.BanMembers)] + [RequireBotDiscordPermissions(DiscordPermission.BanMembers)] [Description("банит пидора")] - public async Task BanAsync([Description("Юзер, кого банить")] IUser user, string reason) { - var banan = new Ban(reason, user); - var embed = new Embed(Colour: _feedbackService.Theme.Secondary, Description: "забанен нахуй"); + public async Task BanUserAsync([Description("Юзер, кого банить")] IUser target, string reason) { + if (!_context.TryGetGuildID(out var guildId)) + return Result.FromError(new ArgumentNullError(nameof(guildId))); + if (!_context.TryGetUserID(out var userId)) + return Result.FromError(new ArgumentNullError(nameof(userId))); + if (!_context.TryGetChannelID(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(channelId))); - return (Result)await _feedbackService.SendContextualEmbedAsync(embed, ct: this.CancellationToken); + var currentUserResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken); + if (existingBanResult.IsDefined(out _)) { + var embed = new EmbedBuilder().WithSmallTitle(Messages.UserAlreadyBanned, currentUser) + .WithColour(Color.Firebrick).Build(); + + if (!embed.IsDefined(out var alreadyBuilt)) + return Result.FromError(embed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(alreadyBuilt, ct: CancellationToken); + } + + var interactionResult + = await _utility.CheckInteractionsAsync(guildId.Value, userId.Value, target.ID, "Ban", CancellationToken); + if (!interactionResult.IsSuccess) + return Result.FromError(interactionResult); + + Result responseEmbed; + if (interactionResult.Entity is not null) { + responseEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, currentUser) + .WithColour(Color.Firebrick).Build(); + } else { + var userResult = await _userApi.GetUserAsync(userId.Value, CancellationToken); + if (!userResult.IsDefined(out var user)) + return Result.FromError(userResult); + + var banResult = await _guildApi.CreateGuildBanAsync( + guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason}", ct: CancellationToken); + if (!banResult.IsSuccess) + return Result.FromError(banResult.Error); + + responseEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserBanned, target.GetTag()), target) + .WithColour(Color.LawnGreen).Build(); + + var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken); + if ((cfg.PublicFeedbackChannel is not 0 && cfg.PublicFeedbackChannel != channelId.Value) + || (cfg.PrivateFeedbackChannel is not 0 && cfg.PrivateFeedbackChannel != channelId.Value)) { + var logEmbed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.UserBanned, target.GetTag()), target) + .WithDescription(string.Format(Messages.DescriptionUserBanned, reason)) + .WithActionFooter(user) + .WithCurrentTimestamp() + .WithColour(Color.Firebrick) + .Build(); + + if (!logEmbed.IsDefined(out var logBuilt)) + return Result.FromError(logEmbed); + + var builtArray = new[] { logBuilt }; + if (cfg.PrivateFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PrivateFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + if (cfg.PublicFeedbackChannel != channelId.Value) + _ = _channelApi.CreateMessageAsync( + cfg.PublicFeedbackChannel.ToDiscordSnowflake(), embeds: builtArray, + ct: CancellationToken); + } + } + + if (!responseEmbed.IsDefined(out var built)) + return Result.FromError(responseEmbed); + + return (Result)await _feedbackService.SendContextualEmbedAsync(built, ct: CancellationToken); } } diff --git a/EventResponders.cs b/EventResponders.cs index d339469..0696581 100644 --- a/EventResponders.cs +++ b/EventResponders.cs @@ -1,7 +1,7 @@ using System.Drawing; using System.Text; using Boyfriend.Data; -using Boyfriend.Data.Services; +using Boyfriend.Services.Data; using DiffPlex; using DiffPlex.DiffBuilder; using Microsoft.Extensions.Logging; @@ -56,7 +56,7 @@ public class GuildCreateResponder : IResponder { var i = Random.Shared.Next(1, 4); var embed = new EmbedBuilder() - .WithTitle(Boyfriend.GetLocalized($"Beep{i}")) + .WithTitle($"Beep{i}".Localized()) .WithDescription(Messages.Ready) .WithUserFooter(currentUser) .WithCurrentTimestamp() @@ -120,7 +120,7 @@ public class MessageDeletedResponder : IResponder { $"{Mention.Channel(gatewayEvent.ChannelID)}\n{Markdown.BlockCode(message.Content.SanitizeForBlockCode())}") .WithActionFooter(user) .WithTimestamp(message.Timestamp) - .WithColour(Color.Crimson) + .WithColour(Color.Firebrick) .Build(); if (!embed.IsDefined(out var built)) return Result.FromError(embed); @@ -153,13 +153,13 @@ public class MessageEditedResponder : IResponder { return Result.FromSuccess(); if (!gatewayEvent.Content.IsDefined(out var newContent)) return Result.FromSuccess(); + if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) + return Result.FromSuccess(); if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); if (!gatewayEvent.ID.IsDefined(out var messageId)) return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); - if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.EditedTimestamp))); var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); var messageResult = await _cacheService.TryGetValueAsync( @@ -286,7 +286,7 @@ public class GuildScheduledEventCreateResponder : IResponder !string.IsNullOrWhiteSpace(piece.Text))) @@ -78,13 +82,13 @@ public static class Extensions { return $"{user.Username}#{user.Discriminator:0000}"; } + public static Snowflake ToDiscordSnowflake(this ulong id) { return DiscordSnowflake.New(id); } - /*public static string AsDuration(this TimeSpan span) { - return span.Humanize( - 2, minUnit: TimeUnit.Second, maxUnit: TimeUnit.Month, - culture: Messages.Culture.Name.Contains("RU") ? GuildConfiguration.CultureInfoCache["ru"] : Messages.Culture); - }*/ + public static TResult? MaxOrDefault( + this IEnumerable source, Func selector) { + return source.Any() ? source.Max(selector) : default; + } } diff --git a/Messages.Designer.cs b/Messages.Designer.cs index 6fea9e3..994a0b2 100644 --- a/Messages.Designer.cs +++ b/Messages.Designer.cs @@ -243,9 +243,9 @@ namespace Boyfriend { } } - internal static string FeedbackUserBanned { + internal static string UserBanned { get { - return ResourceManager.GetString("FeedbackUserBanned", resourceCulture); + return ResourceManager.GetString("UserBanned", resourceCulture); } } @@ -771,15 +771,15 @@ namespace Boyfriend { } } - internal static string LocalEventCreatedDescription { + internal static string DescriptionLocalEventCreated { get { - return ResourceManager.GetString("LocalEventCreatedDescription", resourceCulture); + return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); } } - internal static string ExternalEventCreatedDescription { + internal static string DescriptionExternalEventCreated { get { - return ResourceManager.GetString("ExternalEventCreatedDescription", resourceCulture); + return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); } } @@ -795,15 +795,27 @@ namespace Boyfriend { } } - internal static string LocalEventStartedDescription { + internal static string DescriptionLocalEventStarted { get { - return ResourceManager.GetString("LocalEventStartedDescription", resourceCulture); + return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); } } - internal static string ExternalEventStartedDescription { + internal static string DescriptionExternalEventStarted { get { - return ResourceManager.GetString("ExternalEventStartedDescription", resourceCulture); + return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); + } + } + + internal static string DescriptionUserBanned { + get { + return ResourceManager.GetString("DescriptionUserBanned", resourceCulture); + } + } + + internal static string UserAlreadyBanned { + get { + return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); } } } diff --git a/Messages.resx b/Messages.resx index 608e946..fc6c755 100644 --- a/Messages.resx +++ b/Messages.resx @@ -204,9 +204,9 @@ You need to specify an integer from {0} to {1} instead of {2}! - - Banned {0} for{1}: {2} - + + {0} was banned + That setting doesn't exist! @@ -468,22 +468,28 @@ {0} has created a new event: - + The event will start at {0} in {1} - + The event will start at {0} until {1} in {2} Event details - + The event has lasted for `{0}` - + The event is happening at {0} - + The event is happening at {0} until {1} + + Reason: {0} + + + This user is already banned! + diff --git a/Messages.ru.resx b/Messages.ru.resx index ccea52b..3ed4b49 100644 --- a/Messages.ru.resx +++ b/Messages.ru.resx @@ -204,9 +204,9 @@ Надо указать целое число от {0} до {1} вместо {2}! - - Забанен {0} на{1}: {2} - + + {0} был(-а) забанен(-а) + Такая настройка не существует! @@ -468,22 +468,28 @@ {0} создаёт новое событие: - + Событие пройдёт {0} в канале {1} - + Событие пройдёт с {0} до {1} в {2} Подробнее о событии - + Событие длилось `{0}` - + Событие происходит в {0} - + Событие происходит в {0} до {1} + + Причина: {0} + + + Этот пользователь уже забанен! + diff --git a/Messages.tt-ru.resx b/Messages.tt-ru.resx index 9913b00..973bcca 100644 --- a/Messages.tt-ru.resx +++ b/Messages.tt-ru.resx @@ -204,9 +204,9 @@ выбери число от {0} до {1} вместо {2}! - - забанен {0} на{1}: {2} - + + {0} забанен + такой прикол не существует @@ -468,22 +468,28 @@ {0} создает новое событие: - + движуха произойдет {0} в канале {1} - + движуха будет происходить с {0} до {1} в {2} побольше о движухе - + все это длилось `{0}` - + движуха происходит в {0} - + движуха происходит в {0} до {1} + + причина: {0} + + + этот шизоид уже лежит в бане + diff --git a/Data/Services/GuildDataService.cs b/Services/Data/GuildDataService.cs similarity index 97% rename from Data/Services/GuildDataService.cs rename to Services/Data/GuildDataService.cs index cb5bb0e..2011fca 100644 --- a/Data/Services/GuildDataService.cs +++ b/Services/Data/GuildDataService.cs @@ -1,8 +1,9 @@ using System.Text.Json; +using Boyfriend.Data; using Microsoft.Extensions.Hosting; using Remora.Rest.Core; -namespace Boyfriend.Data.Services; +namespace Boyfriend.Services.Data; public class GuildDataService : IHostedService { private readonly Dictionary _datas = new(); diff --git a/Services/UtilityService.cs b/Services/UtilityService.cs new file mode 100644 index 0000000..2129079 --- /dev/null +++ b/Services/UtilityService.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Hosting; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Services; + +public class UtilityService : IHostedService { + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IDiscordRestUserAPI _userApi; + + public UtilityService(IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) { + _guildApi = guildApi; + _userApi = userApi; + } + + public Task StartAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct) { + return Task.CompletedTask; + } + + public async Task> CheckInteractionsAsync( + Snowflake guildId, Snowflake interacterId, Snowflake targetId, string action, CancellationToken ct = default) { + if (interacterId == targetId) + return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); + + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); + if (!guildResult.IsDefined(out var guild)) + return Result.FromError(guildResult); + + if (targetId == guild.OwnerID) return Result.FromSuccess($"UserCannot{action}Owner".Localized()); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) + return Result.FromError(currentUserResult); + + if (currentUser.ID == targetId) + return Result.FromSuccess($"UserCannot{action}Bot".Localized()); + + if (interacterId == guild.OwnerID) return Result.FromSuccess(null); + + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) + return Result.FromSuccess(null); + + var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, currentUser.ID, ct); + if (!currentMemberResult.IsDefined(out var currentMember)) + return Result.FromError(currentMemberResult); + + var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); + if (!rolesResult.IsDefined(out var roles)) + return Result.FromError(rolesResult); + + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId, ct); + if (!interacterResult.IsDefined(out var interacter)) + return Result.FromError(interacterResult); + + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)); + var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); + var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID)); + + var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.Max(r => r.Position); + var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.Max(r => r.Position); + + if (targetInteracterRoleDiff >= 0) + return Result.FromSuccess($"UserCannot{action}Target".Localized()); + + if (targetBotRoleDiff >= 0) + return Result.FromSuccess($"BotCannot{action}Target".Localized()); + + return Result.FromSuccess(null); + } +}